Compare commits

...
Author SHA1 Message Date
Bart fa50181606 test: Cover visitDifferences node skipping
visitDifferences() is reached in production only through a file-static
populateFetchPack() in LedgerMaster.cpp, so the private hasLeafNode()
and hasInnerNode() it consults were untested. Four cases pin both
answers: only the nodes on a missing item's path, nothing against an
identical map, every node against a null map, and a callback stop.

The first case uses 64 shared items, so the shared leaves build inner
nodes of their own and whole matching subtrees exist to skip. A
finalize() helper seals each map first, since visitDifferences() returns
early while the root hash is zero, and getHash() is what seals it, by
unsharing such a map. Each call is wrapped in ASSERT_NO_FATAL_FAILURE,
which carries a gtest ASSERT_ failure out of a helper to its caller.
2026-10-16 06:37:14 +00:00
Bart 74df6d278f refactor: Derive a node's position from its path, not a stored ID
NodePathStack stored a SHAMapNodeID beside every node, so the path
carried two answers to where a node sits that could disagree. The path
answers it alone: a SHAMap has no path compression, so entry i sits at
depth i. No consumer wanted the ID, only a depth and the nibbles of a
key it already held, so selectBranch() gains a depth overload,
unshareNode() takes a depth, and SHAMap::isLeafDepth() replaces a
file-local copy so one predicate serves every site that judges a depth.

What a path cannot derive is which branches the caller descended, so
pathKey_ records those and a leaf is judged against every branch above
it through samePositionAtDepth(). SharedIntrusive's move constructors
are noexcept, since the path is a vector of them and move_if_noexcept
would otherwise copy each element.
2026-10-16 06:37:13 +00:00
Bart dc55084ee0 refactor: Move entries off NodePathStack instead of copying them
NodePathStack::releaseNode() moves the top node out and pops it, where
five sites copied it out and then popped, each copy costing an atomic
increment and the original's release. dirtyUp() and delItem() walk up to
kLeafDepth levels per insert or delete on the ledger write path. The two
sites that read without popping bind a reference into the container,
sound only because it is a std::stack over a std::deque.

staticPointerCast() gains an rvalue overload, spelled with
SharedIntrusive<TT>&& so an lvalue still picks the const-ref one. Three
dynamic casts become static with a live type test, and a fourth static
cast that had none gains one, reporting UNREACHABLE in dirtyUp() and
delItem()'s loop and answering false in updateGiveItem() and delItem()'s
leaf cast, since the public API permits a call whose tag is absent.
2026-10-16 06:37:13 +00:00
Bart 3b9e8e19f0 fix: Judge a node's position on both getMissingNodes paths
Both getMissingNodes paths judge a leaf's position: the arm that handles
a node descendAsync has already resolved, and the deferred-read hook for
one an asynchronous read resolves later. The deferred-read hook also
refuses an inner node at kLeafDepth, which only the synchronous arm
judged before. gmnProcessDeferredReads() is no longer static so it can
record the verdict, and it keeps draining every read this pass posted
after reaching it, since each holds a pointer to the walk's state. Both
of its tests skip the node rather than return.

A second belongsAt() overload answers about a child from its parent's
position and the branch taken, so neither site builds the child's ID,
and the type is tested first, which keeps the position test off every
inner child. It is reached only for a child the full-below lookup did
not answer for: that lookup decides which children the walk visits, and
this test decides whether a visited leaf belongs where it was put.

With the node flag no longer trusted below the root, this is also what
closes the shared-subtree case: a root naming a completed subtree one
branch over, or using it as a root, now descends it and condemns the
map at the misplaced leaf. The full-below case asserts that outcome.
2026-10-16 06:37:12 +00:00
Bart 079c3b43dd fix: Judge a node's position at its hook and in both full-below memos
descend()'s filter path and addKnownNode()'s leaf hook judge a node's
position before hooking it. setInvalid() and the trySetState() it routes
through are const, so a read-only descend() can record that verdict.

A full-below entry was keyed on the subtree hash alone, so a hit
answered for a hash without answering for a position. FullBelowKey
carries the masked node ID, from the new childNodeID(), and its depth
beside the hash, so a hit answers for one position. The depth is kept
because SHAMapNodeID masks its id to its own depth, leaving the root and
every all-zero prefix sharing one id, and the position is two plain
fields rather than a SHAMapNodeID, which is a CountedObject a cache
entry has no business entering. The key grows from 32 to 68 bytes, so
about 18 MiB more at the kFullBelowTargetSize of 524288 entries.

SHAMapInnerNode::fullBelowGen_ is the second memo with the same flaw.
The TreeNodeCache shares one node object by hash across maps and
positions, and the walk read the flag in its loop condition ahead of the
cache lookup, so a subtree completed at one position was skipped at
another. The flag is now written and read for a walk's root alone, where
the position is fixed, and the position-keyed cache is the only memo
below it. A backed map already consults the cache first, so a warm cache
costs nothing more; an unbacked map re-walks its resident nodes on each
pass. The new case completes a subtree in one map, then checks that a
root naming it one branch over descends it and that the subtree used as
a root is walked rather than trusted.
2026-10-16 06:37:12 +00:00
Bart 962767a9b3 fix: Refuse a node that cannot sit where a path puts it
NodePathStack::pushChild() refuses a node with no room below it at the
depth offered, and a leaf whose key does not lie under the branch
reached. belongsAt() and pastLeafDepth() are the predicates, and
walkTowardsKey()'s no-path mode shares pastLeafDepth(), so both its
modes refuse at the same depth; the no-path mode reports the key
absent, since its callers ask about a key rather than a position. Each reports SOMETIMES, not UNREACHABLE,
since the two-argument descend() judges neither position nor type.
boundHelper() throws where it answered end(), since an empty map still
leaves its root on the path, so an empty path means a node was refused
while end() claims no key lies on the requested side.

A refusal also reaches the empty-path throws in delItem(), addGiveItem()
and updateGiveItem(). OpenLedger::apply() gains in its retry pass the
guard the first pass already had, counting a transaction it cannot apply
as a failure.
2026-10-16 06:37:11 +00:00
Bart 4aea1e20ef fix: Bound the duplicate credit a TX-set acquire grants
A duplicate-only reply postpones at most kMaxDuplicateCredits
consecutive timer intervals with no useful node in between, so the
timeout count always resumes. A batch that advances the set earns the
budget back, and stillNeed() clears it on revival. The credit is spent
only while nothing else has answered the interval, so a wide fan-out
costs one credit rather than one per loser.

Two bounds join it: the sender must be a peer already asked, and the
acquisition must still be running. Both are read in takeNodes() before
takeNodesLocked(), which settles the set on some paths and enrolls the
sender in requestedPeers_ on others. takeNodes() also absorbs the
settled-set branch, so both bounds and the late-reply allowance are
judged ahead of takeNodesLocked().
2026-10-16 06:37:11 +00:00
Bart 206514a024 fix: Keep an acquisition registered when it supplies its own set
giveSet()'s third parameter is now fromAcquire, and the entry's acquire
pointer is reset only when something other than the acquisition supplied
the set. Dropping it cancels an acquisition in flight, which a set
arriving some other way should do and an acquisition completing on its
own should not, so the entry stays registered until newRound() sweeps it
and a late reply still reaches the per-peer allowance. Keying the entry
by hash alone holds because addRootNode() refuses a root that does not
hash to the hash asked for.

wantsReplyFrom() decides ahead of the parse, so a reply turned away is
never built into nodes. It shares chargeLateReply() with
takeNodesLocked(), and only one of the two sees a reply, so one charge.
2026-10-16 06:37:10 +00:00
Bart 0633d5cd6b fix: Fail a receiveNode() packet whose map has been abandoned
receiveNode() asks map.isValid() right after the locked receive and
fails the acquisition without charging the sender, since the verdict
belongs to the walk that reached it rather than to this packet. The test
sits ahead of isSynching(), which reads an abandoned map the same way as
one this packet just finished, because addKnownNode() answers every node
offered to such a map as a duplicate.

A second case pins the other outcome at that site: a packet leaving
nothing to fetch settles the ledger and reports it complete from
receiveNode() itself. DeepChain::allNodes() names the node list, root
included, that two suites assembled by hand.
2026-10-16 06:37:10 +00:00
Bart 6c29093de0 fix: Fail an acquisition whose map cannot exist
A node that leaves a map invalid proves the hash being chased belongs to
no valid tree, so TransactionAcquire::takeNodesLocked() and
InboundLedger::receiveNode() fail the acquisition there rather than
leaving the failure to the next trigger(). The whole batch is discarded,
since the nodes hooked in ahead of that one belong to the same tree.
Such a node costs kFeeMalformedData rather than kFeeInvalidData.

stillNeed() reports whether the set is still worth keeping and refuses
to revive one whose map went invalid. InboundTransactions::getSet()
refreshes an entry's retention window only while that answer is yes, so
such an entry is swept rather than kept alive by the asking.
recordPacket() and haveEverything() name the packet tally and the
nothing-left-to-fetch test that several sites repeated.
2026-10-16 06:37:09 +00:00
Bart 16187ab684 fix: Charge declined TX-set data where it was declined
takeNodesLocked() issues the charge for declined data, under the lock
that reached the verdict: an empty node list, a root that does not hash
to the set asked for, a node that cannot be hooked, and node data that
will not deserialize each cost kFeeInvalidData at the site rejecting it.
A batch whose parse throws is tallied as invalid but not charged, since
what throws there is this code rather than the data.

A reply arriving after the acquisition has failed is free once per peer:
each peer holds one unspent pass, renewed by each request the acquisition
sends it, so overlapping requests to one peer share one pass, and a reply
past that pass costs kFeeUselessData. requestedPeers_ records every peer
a request went to, since trigger() can also send a targeted request to an
unsolicited sender, and it records one where it builds the request, so
selection alone does not enroll a peer. lateReplyGranted_ keys the
allowance by peer identity rather than counting it, since a count cannot
say whose pass a reply spends. recordAsked() renews a peer's pass with
every request that reaches it, targeted or broadcast; a revival on its
own renews nothing. A reply to a completed set still costs
kFeeUselessData, since giveSet() drops the acquisition.

That settled reply reports a duplicate, so takeNodes() reads whether the
set was settled when the call arrived and grants the timer credit only
to a reply the running acquisition earned.
2026-10-16 06:37:08 +00:00
Bart 28f83b5bcf fix: Count partial batch progress in a TX-set reply
TransactionAcquire::takeNodes() accumulates one SHAMapAddNode across the
whole batch, so a packet that ends on a rejected node still counts the
nodes hooked in ahead of it, as InboundLedger::receiveNode() does. The
body moves to takeNodesLocked() and takeNodes() records progress on the
single exit, since several inner returns stop the batch early.

Progress is recorded for a batch that hooked a node in or answered with
one already held. A batch of nodes already held is what an honest second
responder to trigger()'s fan-out sends, so gotData() reads isGood()
rather than isUseful() when it decides whether to charge for useless
data.
2026-10-16 06:37:08 +00:00
Bart 67271ddc01 fix: Restart the timer when reviving a timed-out TX-set acquire
TransactionAcquire::stillNeed() restarts the retry timer whenever it
revives a failed acquisition, so a revived object resumes asking rather
than waiting for a peer to send data unprompted. expires_after() cancels
whatever wait was outstanding, so the timer holds at most one wait at a
time. A job already handed to the JobQueue is not canceled and re-arms
this same timer, which folds back into the one chain. A running
acquisition returns early and keeps the wait it has, so a consensus
round that asks for the set again does not restart it. The timeout count
is clamped either way.
2026-10-16 06:37:07 +00:00
Bart 2f9e1b1716 fix: Start a ledger built from a header mutable
Ledger(LedgerHeader const&, Rules, Family&) starts at immutable_ false.
Its maps are constructed Synching and filled in afterwards by an
acquisition or a replay, so setImmutable() is what settles the ledger,
and only once both maps are sound. mapHashesFromHeader_ records that
this constructor's transaction and account hashes are input rather than
derived, so setImmutable() leaves them alone: deriving them from the
maps would relabel a map that fell short of its target. The flag is
const, unlike immutable_.

A ledger this constructor built is therefore unsettled until
setImmutable() says otherwise, and LedgerMaster::switchLCL() and
LedgerHistory::insert() each require a settled one.
InboundLedger::getLedger() already reports nothing once the acquisition
has failed, so done()'s posted job reads the accessor: failure and a
null ledger_ are refused by one check, and checkAccept() requires a
non-null one. isFailed()'s comment said the opposite of what it returns.
2026-10-16 06:37:07 +00:00
Bart d7988a2579 fix: Settle an acquired ledger before reporting it complete
done() owns the publication of complete_ and sets it only once the
ledger is settled, so a caller reading isComplete() without mtx_ may use
the ledger directly. LedgerHistory::insert(), LedgerHolder::set() and
LedgerMaster::switchLCL() each require an immutable ledger, and that
ordering is what their requirement rests on. trigger() and receiveNode()
set the have-flags and leave the verdict to done(), while tryDB()
settles its own result and sets complete_ itself, so that path arrives
with only the reporting left.
2026-10-16 06:37:06 +00:00
Bart cf7d48d117 fix: Publish an acquisition's outcome flags across threads
complete_ and failed_ are std::atomic<bool> at the default seq_cst
order, so a reader that sees either flag set also sees the work the
writer did before setting it. isComplete() and isFailed() return them
without taking mtx_, and InboundLedgers::acquire() calls both outside
every lock it holds. A static_assert pins both to a lock-free
representation, so neither accessor can block. No access site changes: a
plain assignment and a plain test compile unchanged, and nothing takes a
reference or an address of either field.

Scoped to those two, which are the only fields here reached through an
accessor that takes no lock.
2026-10-16 06:37:06 +00:00
Bart 495374869a fix: Tell a satisfied InboundLedger map from an abandoned one
hasInvalidMap() reports whether either map of the ledger being acquired
has been found invalid, and the three places that read an empty walk
result as nothing left to fetch ask it first: tryDB(), whose two walks
set haveState_ and haveTransactions_ independently, trigger()'s
aggressive-retry branch, whose getNeededHashes() walk can reach the
verdict itself, and trigger()'s state-map walk, the one walk that runs
with mtx_ released.

The state-map walk asks outside the guard that re-reads the flags after
re-locking, since the verdict is about the map rather than the round,
and it withdraws the completeness claim beside the failure.
2026-10-16 06:37:04 +00:00
BartandBart 4087861411 fix: Refuse to walk an invalid SHAMap in getMissingNodes (#8086)
Co-authored-by: Bart <[email protected]>
2026-10-16 05:47:08 +00:00
BartandBart dc43faafed fix: Refuse to make an invalid SHAMap or Ledger immutable (#8085)
Co-authored-by: Bart <[email protected]>
2026-10-16 05:47:07 +00:00
6af97c07d9 fix: Report a node the sync path refuses as invalid data (#8084)
Co-authored-by: Bart <[email protected]>
Co-authored-by: Claude Fable 5.1 <[email protected]>
2026-10-16 05:47:06 +00:00
4c7d920e66 fix: Make the SHAMap sync-path state atomic (#8083)
Co-authored-by: Bart <[email protected]>
Co-authored-by: Claude Fable 5.1 <[email protected]>
2026-10-16 05:47:06 +00:00
yinyiqian1 8b59d9d307 docs: Add todo comment for key rotation zkproof (#8589) 2026-10-16 00:11:06 +00:00
b0fa295595 fix: Judge a header's account hash on both acquisition routes (#8476)
Co-authored-by: Bart <[email protected]>
Co-authored-by: Claude Fable 5.1 <[email protected]>
2026-10-15 21:00:21 +00:00
BartandBart 9af432cb0e fix: Signal every InboundLedger failure found in local data (#8082)
Co-authored-by: Bart <[email protected]>
2026-10-15 21:00:20 +00:00
BartandBart 4435ca4cbf refactor: Add a reusable peer harness for acquisition tests (#8310)
Co-authored-by: Bart <[email protected]>
2026-10-15 21:00:19 +00:00
Olek cd72a11786 refactor: TxQ (#8064) 2026-10-15 18:11:46 +00:00
Peter Chen b398b88385 fix: Reject a zero credential ID in deposit_authorized (#8533) 2026-10-15 18:11:18 +00:00
Olek 31f6f90206 feat: Add IssuerMirrorEncryptionKey and InitialIssuerEncryptionKey (#8325) 2026-10-15 18:04:29 +00:00
Olek 1d8c2f9172 test: Sponsor checkSign additional tests (#8331) 2026-10-15 18:03:48 +00:00
f105418bdb feat: Book FixedPrecision LoanPay and LoanManage (#8271)
Co-authored-by: Jingchen <[email protected]>
Co-authored-by: Alex Kremer <[email protected]>
2026-10-15 18:02:36 +00:00
Ayaz Salikhov 4a134eb883 docs: Clarify OS and toolchain requirements (#8588) 2026-10-15 17:58:11 +00:00
Ayaz Salikhov ec3f251ad8 ci: Fix running pre-commit after clang-tidy (#8601) 2026-10-15 16:15:35 +00:00
Mayukha Vadari 5faa861226 feat: Add Smart Escrow fee voting (#8214) 2026-10-15 15:50:16 +00:00
Jingchen 88fd1222d6 build: Fix unity build by addressing name clashes (#8599) 2026-10-15 12:59:20 +00:00
Ayaz SalikhovandEd Hennis f1ce9449f2 fix: Skip bootstrap DNS lookups in standalone mode (#8583)
Co-authored-by: Ed Hennis <[email protected]>
2026-10-14 21:02:45 +00:00
Timur Yalymov 7237b3321c fix: Charge transfer fee on third-party vault withdrawals (#8267) 2026-10-14 16:35:42 +00:00
Timur Yalymov 84d980d883 fix: Reject VaultClawback with a nonexistent holder (#8470) 2026-10-14 14:29:05 +00:00
Timur YalymovandVito Tumas 791c96faad fix: Enforce pseudo-account directory contents with an invariant (#8181)
Co-authored-by: Vito Tumas <[email protected]>
2026-10-14 13:51:24 +00:00
Ayaz Salikhov 31473e7e6d build: Unify clang variables (#8581) 2026-10-14 13:51:05 +00:00
Timur YalymovandVito Tumas aa70b368b5 fix: Enforce asset authorization on LoanBrokerDelete (#8204)
Co-authored-by: Vito Tumas <[email protected]>
2026-10-14 13:51:05 +00:00
Vito TumasandJingchen d3db569b34 feat: Book FixedPrecision LoanSet through AssetsDeployed (#8260)
Co-authored-by: Jingchen <[email protected]>
2026-10-14 13:20:58 +00:00
Vito TumasandJingchen 94d6b7daf8 feat: Round LoanBroker cover on the FixedPrecision grid (#8258)
Co-authored-by: Jingchen <[email protected]>
2026-10-14 12:59:39 +00:00
Vito TumasandJingchen e089abe982 feat: Use FixedPrecision accounting in Vault transactors (#8460)
Co-authored-by: Jingchen <[email protected]>
2026-10-14 12:52:25 +00:00
Alex Kremer 33289e882b refactor: Migrate handlers to rpc-spec (C) (#8561) 2026-10-14 10:15:32 +00:00
1ed0bccb07 fix: Clamp invalid XRP amounts in Quality::ceilOut & getBookPage (#8480)
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Ed Hennis <[email protected]>
2026-10-14 07:49:27 +00:00
Gregory TsipenyukandEd Hennis 1da55e3989 fix: Waive CanTransfer on loan default and broker delete (#8231)
Co-authored-by: Ed Hennis <[email protected]>
2026-10-14 07:49:12 +00:00
Valentin Balaschenkoandxrplf-ai-reviewer[bot] 166e97e12b refactor: Decouple LedgerHistory from TaggedCache internals (#5874)
Co-authored-by: xrplf-ai-reviewer[bot] <266832837+xrplf-ai-reviewer[bot]@users.noreply.github.com>
2026-10-13 23:33:32 +00:00
33e5a6fa7c feat: Add TransactionProposalCancel & Cosign Ticket reservation (#8538)
Co-authored-by: Shawn Xie <[email protected]>
Co-authored-by: Kassaking7 <[email protected]>
Co-authored-by: Shawn Xie <[email protected]>
Co-authored-by: Zhiyuan Wang <[email protected]>
Co-authored-by: Claude Fable 5 <[email protected]>
2026-10-13 19:07:45 +00:00
Ayaz Salikhov b1f9dc6919 ci: Fix rpm installation problems (#8575) 2026-10-13 18:46:41 +00:00
Ayaz Salikhov 38965af055 docs: Clarify freeze on large changes 2 weeks before first RC (#8574) 2026-10-13 18:43:24 +00:00
Gregory TsipenyukandEd Hennis b010d4a563 refactor: Retire fixAMMv1_1 amendment (#8226)
Co-authored-by: Ed Hennis <[email protected]>
2026-10-13 18:03:10 +00:00
dependabot[bot] eedd6604d1 chore: [DEPENDABOT] bump pyparsing from 3.3.2 to 3.3.3 in /cmake/scripts/codegen in the codegen-dependencies group (#8572)
Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-13 17:48:57 +00:00
Mayukha Vadari 59ff04b648 fix: Allow WASM transactions in simulate only in admin mode (#8568) 2026-10-13 17:39:32 +00:00
Gregory TsipenyukandEd Hennis fcb3ee6425 fix: Detect int64 overflow in XRPAmount and MPTAmount (#8453)
Co-authored-by: Ed Hennis <[email protected]>
2026-10-13 17:35:35 +00:00
Gregory TsipenyukandEd Hennis 77eacbb52c fix: Assorted MPT/DEX offer related fixes (#8211)
Co-authored-by: Ed Hennis <[email protected]>
2026-10-13 17:18:47 +00:00
245 changed files with 33597 additions and 4819 deletions

No files matched your search

+2
View File
@@ -103,6 +103,7 @@ words:
- dearmor
- decryptor
- dedented
- dedup
- deleteme
- demultiplexer
- deserializaton
@@ -344,6 +345,7 @@ words:
- unambiguity
- unauthorizes
- unauthorizing
- undeserializable
- unergonomic
- unfetched
- unfindable
@@ -62,6 +62,7 @@ libxrpl.tx > xrpl.protocol
libxrpl.tx > xrpl.server
libxrpl.tx > xrpl.tx
test.app > test.jtx
test.app > tests.libxrpl
test.app > test.unit_test
test.app > xrpl.basics
test.app > xrpl.config
+8 -2
View File
@@ -123,13 +123,19 @@ jobs:
- name: Fix style
if: ${{ steps.files_changed.outcome != 'success' }}
# pre-commit installs every hook's environment, even for hooks that match
# no files, and node-based hooks need libatomic, which this image lacks.
# Exit code 1 just means hooks modified files or found issues.
env:
SKIP: prettier,cspell
run: |
pre-commit run --all-files || true
git add --update
pre-commit run || [ $? -eq 1 ]
- name: Generate git diff
if: ${{ steps.files_changed.outcome != 'success' }}
run: |
git diff | tee "${DIFF_FILE}"
git diff HEAD | tee "${DIFF_FILE}"
- name: Upload clang-tidy diff output
if: ${{ github.event.repository.visibility == 'public' && steps.files_changed.outcome != 'success' }}
@@ -95,11 +95,15 @@ jobs:
apt-get update -qq
apt-get install -y "./${PACKAGE}"
# Rocky Linux and AlmaLinux pick a mirror from a mirrorlist, and now and
# then get one that serves the repository metadata at a few hundred kB/s,
# which alone outlasts the job's timeout. dnf only abandons a mirror below
# 1000 B/s for 30 s, so raise that floor to fail over to the next one fast.
- name: Install the RPM
if: ${{ inputs.package_type == 'rpm' }}
env:
PACKAGE: ${{ steps.find.outputs.package }}
run: dnf install -y "./${PACKAGE}"
run: dnf install -y --setopt=minrate=1M --setopt=timeout=10 "./${PACKAGE}"
- name: Run xrpld
run: xrpld --version
+2
View File
@@ -35,6 +35,8 @@ Version 3.5.0 is not yet released.
- `channel_authorize`: The `channel_id` field now returns an `invalidParams` error if the value is not a string. [#7582](https://github.com/XRPLF/rippled/pull/7582)
- `channel_verify`: The `channel_id` and `signature` fields now return an `invalidParams` error if the value is not a string. [#7582](https://github.com/XRPLF/rippled/pull/7582)
- `account_currencies`, `account_nfts`, `account_offers`: A request with more than one problem may now report a different one of them. An invalid `ledger_hash` or `ledger_index` is reported before an invalid or missing `account`. An invalid `limit` or `marker` is reported before `lgrNotFound` or `actNotFound`, and for `account_offers` also before `actMalformed`. A request with a single problem returns the same error as before. [#8561](https://github.com/XRPLF/rippled/pull/8561)
- `deposit_authorized`: A `credentials` entry that is all zeros (for example `"0"` or 64 `0` characters) now returns a `badCredentials` error instead of looking up a credential that can never exist.
### Bugfixes in 3.5.0
+5 -26
View File
@@ -2,33 +2,12 @@
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| These instructions assume you have a C++ development environment ready with Git, Python, Conan, CMake, and a C++ compiler. For help setting one up on Linux, macOS, or Windows, [see this guide](./docs/build/environment.md).<br><br>These instructions also assume a basic familiarity with Conan and CMake. If you are unfamiliar with Conan, you can read our [crash course](./docs/build/conan.md) or the official [Getting Started][conan-getting-started] walkthrough. |
## Minimum Requirements
## Supported platforms
For the hardware needed to run a node, see
[System Requirements](https://xrpl.org/system-requirements.html).
For the software needed to build xrpld, see the
[environment setup guide](./docs/build/environment.md).
## Operating Systems
### Linux
The Ubuntu Linux distribution has received the highest level of quality
assurance, testing, and support. We also support Red Hat and use Debian
internally.
Our Linux CI tooling is distro-independent and uses a Nix-based environment, so it should be possible to build on other Linux distributions as well, although we have not tested them.
### macOS
Many `xrpld` engineers use macOS for development.
The minimum supported version is macOS 15 (Sequoia).
CI testing is done in macOS 26 (Tahoe), but the build defaults `CMAKE_OSX_DEPLOYMENT_TARGET` to 15.
### Windows
Windows is used by some engineers for development only, and is not recommended
for production use.
Building and testing are supported on Linux, macOS, and (best effort) Windows;
see [Supported development platforms](./docs/build/environment.md#supported-development-platforms).
Running `xrpld` is officially supported on Linux only;
see [Requirements](./docs/install.md#requirements).
## Steps
+1 -1
View File
@@ -42,7 +42,7 @@ if(is_gcc)
# GCC-specific fixes
add_compile_options(-Wno-unknown-pragmas -Wno-subobject-linkage)
# -Wno-subobject-linkage can be removed when we upgrade GCC version to at least 13.3
elseif(is_clang)
elseif(is_any_clang)
# Clang-specific fixes
add_compile_options(-Wno-unknown-warning-option) # Ignore unknown warning options
elseif(is_msvc)
+1 -1
View File
@@ -332,7 +332,7 @@ This ensures that configuration changes don't introduce new warnings across the
### Installing clang-tidy
See the [environment setup guide](./docs/build/environment.md#clang-tidy) for how to get clang-tidy.
The [Nix development shell](./docs/build/nix.md) provides `clang-tidy` at the [tested version](./docs/build/environment.md#tested-compiler-versions).
### Running clang-tidy locally
+16 -10
View File
@@ -34,7 +34,7 @@ so tools can filter them and protection rules can target `release/*` and `stagin
- **Branches only move forward.**
`develop`, `staging/X.Y.x`, and `release/X.Y.x` are never rewritten.
- **Cherry-picking only goes from `develop` to a staging branch**,
for fixes that must get into a release after the code freeze
for fixes that must get into a release after the [code freeze](#release-candidates)
(see [Emergency Fixes From `develop`](#emergency-fixes-from-develop)).
- **Security fixes are prepared privately and published with the release that contains them**,
so vulnerabilities are not disclosed prematurely.
@@ -49,6 +49,7 @@ develop staging/X.Y.x & release/X.Y.x
│
├── Tag: X.Y.0-b1
├── Tag: X.Y.0-bN
│ (2 weeks before rc1: freeze on large changes starts)
├── Tag: X.Y.0-rc1 ───────────────┐ (branches created)
│ │
│ (development continues) ├── Fixes
@@ -75,16 +76,20 @@ develop staging/X.Y.x & release/X.Y.x
3. **Branches:** `staging/X.Y.x` and `release/X.Y.x` are created from `develop`
at the commit tagged `X.Y.0-rc1`.
The first RC also kicks off the QE process.
4. **Code freeze:** No new features or unrelated changes are pulled from `develop`
into `staging/X.Y.x` or `release/X.Y.x`.
Only critical stabilization fixes go into the line.
5. **No large changes on `develop`:** Until `X.Y.0` is [merged back](#merging-back-into-develop),
large changes (e.g., big refactors, moving or renaming many files, mass reformatting)
are not merged into `develop`,
so that fixes on the line and the merge back don't run into conflicts.
### Freeze on Large Changes
From two weeks before `X.Y.0-rc1` until `X.Y.0` is [merged back](#merging-back-into-develop),
large changes (e.g., big refactors, moving or renaming many files, mass reformatting)
are not merged into `develop`.
### Release Candidates
After branching, the line is in code freeze:
no new features or unrelated changes are pulled from `develop`
into `staging/X.Y.x` or `release/X.Y.x`.
Only critical stabilization fixes go into the line.
1. Fixes are developed against `staging/X.Y.x`.
2. When ready, a new RC is created on `staging/X.Y.x`,
and `release/X.Y.x` is fast-forwarded to it.
@@ -106,7 +111,8 @@ one right after the other.
2. `release/X.Y.x` is fast-forwarded to `staging/X.Y.x`,
and the release is tagged on it.
3. The release is immediately [merged back into `develop`](#merging-back-into-develop).
For a major-minor release, this lifts the freeze on large changes in `develop`.
For a major-minor release, this lifts the [freeze on large changes](#freeze-on-large-changes)
in `develop`.
### Merging Back Into `develop`
@@ -127,7 +133,7 @@ After the merge, `git log develop..<tag>` must be empty.
### Emergency Fixes From `develop`
If a commit was merged to `develop`
and needs to be included in a release after the code freeze:
and needs to be included in a release after the [code freeze](#release-candidates):
1. Create a PR that cherry-picks the commit onto `staging/X.Y.x`.
2. Leave `develop` as it is, with no reverts.
+33
View File
@@ -1360,6 +1360,39 @@
# Example:
# owner_reserve = 200000 # 0.2 XRP
#
# gas_limit = <gas>
#
# The gas limit is the maximum amount of gas that can be
# consumed by a single transaction. The gas limit is used to prevent
# transactions from consuming too many resources.
#
# If this parameter is unspecified, xrpld will use an internal
# default. Don't change this without understanding the consequences.
#
# Example:
# gas_limit = 1000000 # 1 million gas
#
# bytecode_size_limit = <bytes>
#
# The bytecode size limit is the maximum size of a WASM extension in
# bytes. The size limit is used to prevent extensions from consuming
# too many resources.
#
# If this parameter is unspecified, xrpld will use an internal
# default. Don't change this without understanding the consequences.
#
# Example:
# bytecode_size_limit = 100000 # 100 kb
#
# gas_price = <micro-drops>
#
# The gas price is the conversion between WASM gas and its price in drops.
#
# If this parameter is unspecified, xrpld will use an internal
# default. Don't change this without understanding the consequences.
#
# Example:
# gas_price = 1000000 # 1 drop per gas
#-------------------------------------------------------------------------------
#
# 9. Misc Settings
+11 -3
View File
@@ -9,13 +9,21 @@ include_guard(GLOBAL)
# --------------------------------------------------------------------
# Compiler detection (C++)
# --------------------------------------------------------------------
set(is_clang FALSE)
# is_any_clang is true for both LLVM Clang and AppleClang. Their version numbers
# and feature sets differ, so use is_llvm_clang or is_apple_clang when it matters.
set(is_any_clang FALSE)
set(is_llvm_clang FALSE)
set(is_apple_clang FALSE)
set(is_gcc FALSE)
set(is_msvc FALSE)
set(is_xcode FALSE)
if(CMAKE_CXX_COMPILER_ID MATCHES ".*Clang") # Clang or AppleClang
set(is_clang TRUE)
if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
set(is_any_clang TRUE)
set(is_llvm_clang TRUE)
elseif(CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang")
set(is_any_clang TRUE)
set(is_apple_clang TRUE)
elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
set(is_gcc TRUE)
elseif(MSVC)
+6 -3
View File
@@ -130,7 +130,7 @@ else()
INTERFACE
-Wall
-Wdeprecated
$<$<BOOL:${is_clang}>:-Wno-deprecated-declarations>
$<$<BOOL:${is_any_clang}>:-Wno-deprecated-declarations>
$<$<BOOL:${wextra}>:-Wextra
-Wno-unused-parameter>
$<$<BOOL:${werr}>:-Werror>
@@ -177,7 +177,7 @@ else()
if(
is_nix_compiler
AND is_linux
AND is_clang
AND is_any_clang
AND CMAKE_CXX_FLAGS MATCHES "stdlib=libstdc"
)
string(
@@ -204,7 +204,10 @@ if(voidstar)
"Antithesis instrumentation requires Linux, aborting..."
)
elseif(
NOT (is_clang AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 16.0)
NOT (
is_llvm_clang
AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 16.0
)
)
message(
FATAL_ERROR
+7 -1
View File
@@ -296,7 +296,13 @@ if(xrpld)
OUT_VAR rpcspec_instantiations
VALUE_TYPE "::json::Value"
VIEW_HEADER "xrpld/rpc/detail/JsonObjectView.hpp"
HANDLERS book_changes ledger transaction_entry
HANDLERS
account_currencies
account_nfts
account_offers
book_changes
ledger
transaction_entry
)
target_sources(xrpld PRIVATE ${rpcspec_instantiations})
+1 -1
View File
@@ -58,7 +58,7 @@ target_link_options(common INTERFACE ${sanitizers_linker_flags})
# This module appends -fsanitize-ignorelist=<path> for Clang builds.
# The ignorelist path contains CMAKE_SOURCE_DIR, so it must be set here, rather than in the Conan profile.
# GCC does not support -fsanitize-ignorelist.
if(is_clang)
if(is_any_clang)
set(ignorelist_path
"${CMAKE_SOURCE_DIR}/sanitizers/suppressions/sanitizer-ignorelist.txt"
)
+2 -5
View File
@@ -23,11 +23,8 @@ if(NOT is_multiconfig)
endif()
endif()
if(is_clang) # both Clang and AppleClang
if(
"${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang"
AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS 16.0
)
if(is_llvm_clang)
if(CMAKE_CXX_COMPILER_VERSION VERSION_LESS 16.0)
message(FATAL_ERROR "This project requires clang 16 or later")
endif()
elseif(is_gcc)
+3 -3
View File
@@ -62,11 +62,11 @@ if(unity)
set(CMAKE_UNITY_BUILD ON CACHE BOOL "Do a unity build")
endif()
if(is_clang AND is_linux)
if(is_llvm_clang AND is_linux)
option(voidstar "Enable Antithesis instrumentation." OFF)
endif()
if(is_gcc OR is_clang)
if(is_gcc OR is_any_clang)
include(ProcessorCount)
ProcessorCount(PROCESSOR_COUNT)
@@ -125,7 +125,7 @@ else()
set(use_mold OFF CACHE BOOL "mold linker, linux only" FORCE)
endif()
if(is_clang)
if(is_any_clang)
option(use_lld "enables detection of lld linker" ON)
else()
set(use_lld OFF CACHE BOOL "try lld linker, clang only" FORCE)
+1 -1
View File
@@ -7,7 +7,7 @@
pcpp>=1.30
# Parser combinator library - used to parse the macro DSL
pyparsing>=3.0.0
pyparsing>=3.3.3
# Template engine - used to generate C++ code from templates
Mako>=1.4.2
+3 -3
View File
@@ -99,7 +99,7 @@ pcpp==1.30 \
--hash=sha256:05fe08292b6da57f385001c891a87f40d6aa7f46787b03e8ba326d20a3297c6e \
--hash=sha256:5af9fbce55f136d7931ae915fae03c34030a3b36c496e72d9636cedc8e2543a1
# via -r requirements.in
pyparsing==3.3.2 \
--hash=sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d \
--hash=sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc
pyparsing==3.3.3 \
--hash=sha256:928ae7e20211f3b6f3915a72f06a0cfd29ab9d24279dd6346b6b1a7146397d36 \
--hash=sha256:ece8c00a69cf01b45d0b1dedabb469c90d8caf996d4fda40f147627a122849a4
# via -r requirements.in
+1 -1
View File
@@ -3,7 +3,7 @@
"requires": [
"zlib/1.3.2#1cb806da49011867778ffb6ac7190fcb%1782392402.122708",
"xxhash/0.8.3#681d36a0a6111fc56e5e45ea182c19cc%1782392402.420688",
"xrpl-rpc-spec/0.1.21#d536f87a2ae7d313452746cfa3ac4404%1790869005.122975",
"xrpl-rpc-spec/0.1.23#56e8946f5b3fb949a3732e1a51135ca2%1790977482.197086",
"sqlite3/3.53.0#324ada52333108388a9a6108bfa96734%1782392403.185447",
"soci/4.0.3#e726491a03468795453f7c83fc924a96%1782392402.679521",
"snappy/1.1.10#968fef506ff261592ec30c574d4a7809%1782307151.633168",
+1 -1
View File
@@ -40,7 +40,7 @@ class Xrpl(ConanFile):
"nudb/2.0.9",
"openssl/3.6.3",
"soci/4.0.3",
"xrpl-rpc-spec/0.1.21",
"xrpl-rpc-spec/0.1.23",
"zlib/1.3.2",
]
+32 -77
View File
@@ -4,6 +4,18 @@ This document explains how to set one up.
[BUILD.md]: ../../BUILD.md
## Supported development platforms
- **Linux**: the [Nix development shell](./nix.md),
which provides the same toolchain CI uses on any distribution.
- **macOS** 15 (Sequoia) or newer: the [Nix development shell](./nix.md),
or the system [Apple Clang](#macos-apple-clang).
CI runs on macOS 26 (Tahoe), but the build defaults `CMAKE_OSX_DEPLOYMENT_TARGET` to 15.
- **Windows**: best effort, see [Windows](#windows).
For the platforms supported for running `xrpld`,
see [Requirements](../install.md#requirements).
## Tested compiler versions
`xrpld` is built in the **C++23** dialect by default, so your toolchain has to
@@ -35,20 +47,16 @@ compiler building. Treat support for anything outside the table as best-effort.
## Required tools
Besides a compiler, building `xrpld` requires:
| Tool | Minimum version |
| ------------------------------------------- | --------------- |
| [Git](https://git-scm.com/downloads) | any recent |
| [Python](https://www.python.org/downloads/) | 3.11 |
| [Conan](https://conan.io/downloads.html) | 2.17 |
| [CMake](https://cmake.org/download/) | 3.16 |
Besides a compiler, building `xrpld` requires
[Git](https://git-scm.com/downloads),
[Python](https://www.python.org/downloads/),
[Conan](https://conan.io/downloads.html),
and [CMake](https://cmake.org/download/).
Building with `-Drust=ON` additionally requires a Rust toolchain,
see [Rust](#rust).
On Linux and macOS, the [Nix development shell](./nix.md) provides all of them
(see below). On Windows they have to be installed manually.
Building with `-Drust=ON` additionally requires a Rust toolchain, see
[Rust](#rust). A default build does not, so it is not in the table above.
at the versions CI uses, recorded in [`nix/check-tools/`](../../nix/check-tools).
Once they are in place, verify that everything is installed and runnable with:
@@ -58,60 +66,19 @@ Once they are in place, verify that everything is installed and runnable with:
## Linux and macOS
The **recommended way** to get a development environment on Linux and macOS is
the Nix development shell. It provides the exact tooling used in CI — `git`,
`python`, `conan`, `cmake`, `clang-tidy`, `clang-format`, and everything else —
with a single command and without installing anything system-wide:
Use the [Nix development shell](./nix.md):
it provides the compiler and every tool CI uses,
without installing anything system-wide.
```bash
nix --experimental-features 'nix-command flakes' develop
```
### macOS: Apple Clang
On **Linux**, Nix also provides the compiler (GCC); on **macOS**, it provides
Clang. If you instead opt to use your system-wide Apple Clang (via
`nix develop .#apple-clang`), you need to manage its version yourself (see
below).
See [Using the Nix development shell](./nix.md) for installation and usage
details, including how to select a different compiler and why we recommend Nix
over a hand-maintained environment.
### macOS: managing the Apple Clang version
If you use your system-wide Apple Clang on macOS (via `nix develop .#apple-clang`),
the compiler version is whatever your installed Xcode (or Command Line Tools)
provides. The following command should return a version greater than or equal to
the [tested one](#tested-compiler-versions):
```bash
clang --version
```
If you develop other applications using Xcode, you might be consistently
updating to the newest version of Apple Clang, which will likely cause issues
building xrpld. You may want to install and pin a specific version of Xcode:
1. **Download Xcode**
- Visit [Apple Developer Downloads](https://developer.apple.com/download/more/)
- Sign in with your Apple Developer account
- Search for an Xcode version that includes the expected Apple Clang version
- Download the `.xip` file
2. **Install and configure Xcode**
```bash
# Extract the .xip file and rename for version management
# Example: Xcode_16.2.app
# Move to Applications directory
sudo mv Xcode_16.2.app /Applications/
# Set as default toolchain (persistent)
sudo xcode-select -s /Applications/Xcode_16.2.app/Contents/Developer
# Set as environment variable (temporary)
export DEVELOPER_DIR=/Applications/Xcode_16.2.app/Contents/Developer
```
You can also build with the system Apple Clang,
installing the other [required tools](#required-tools) any way you like
(or getting them from `nix develop .#apple-clang`).
Its version is whatever your installed Xcode (or Command Line Tools) provides,
so check that `clang --version` is at least the [tested one](#tested-compiler-versions).
If several Xcode versions are installed,
select one with `sudo xcode-select -s <path>` or the `DEVELOPER_DIR` environment variable.
## Windows
@@ -123,8 +90,7 @@ manually:
"x64 Native Tools Command Prompt". CI configures CMake with the
`Visual Studio 18 2026` generator.
- [Git for Windows](https://git-scm.com/download/win)
- Python, Conan, and CMake, at the versions listed in
[Required tools](#required-tools).
- recent releases of Python, Conan, and CMake
- a [Rust toolchain](https://rustup.rs) — only needed to build with
`-Drust=ON`, see [Rust](#rust)
@@ -145,16 +111,5 @@ version.
Everything else the Rust build needs on the CMake side comes from Conan along
with the rest of the dependencies, so there is nothing further to install.
## Clang-tidy
`clang-tidy` is required to run static analysis checks locally (see
[CONTRIBUTING.md](../../CONTRIBUTING.md)). It is not required to build the
project. The version this project uses is listed in
[Tested compiler versions](#tested-compiler-versions).
On Linux and macOS, the [Nix development shell](./nix.md) provides that exact
version out of the box — run it via `run-clang-tidy`. No separate installation
is needed.
[cpp23-support]: https://en.cppreference.com/w/cpp/compiler_support/23
[packages.nix]: ../../nix/packages.nix
+2 -9
View File
@@ -9,9 +9,7 @@ This guide explains how to use Nix to set up a reproducible development environm
- **Reproducible environment**: Everyone gets the same versions of tools and compilers
- **Matches CI**: The Linux CI runs in Docker images built from this exact Nix environment, and CI builds some macOS configurations in it as well
- **No system pollution**: Dependencies are isolated and don't affect your system packages
- **Consistent compilers**: The GCC and Clang shells use the same versions as CI
- **Quick setup**: Get started with a single command
- **Works on Linux and macOS**: Consistent experience across platforms
## Install Nix
@@ -27,13 +25,8 @@ From the root of the xrpld repository, enter the default development shell:
nix --experimental-features 'nix-command flakes' develop
```
This will:
- Download and set up all required development tools (CMake, Ninja, Conan, etc.)
- Configure the appropriate compiler for your platform:
- **Linux**: GCC (provided by Nix)
- **macOS**: Clang (provided by Nix)
This downloads and sets up all required development tools (CMake, Ninja, Conan, etc.)
and the compiler for your platform (see [Platform notes](#platform-notes)).
The first time you run this command, it will take a few minutes to download and build the environment. Subsequent runs will be much faster.
### Platform notes
+13
View File
@@ -10,6 +10,19 @@ and DNF on Red Hat-based distributions such as RHEL, AlmaLinux, and Rocky Linux,
where `yum` is a symlink to `dnf`.
To build from source instead, see [BUILD.md](../BUILD.md).
## Requirements
Linux is the only operating system officially supported for running `xrpld`;
macOS and Windows builds are for development only.
Any x86-64 Linux distribution with APT or DNF and glibc 2.31 or newer should work
(see [How libc is handled](../nix/docker/README.md#how-libc-is-handled)).
CI tests the packages on Debian 11–13, Ubuntu 20.04–26.04,
RHEL (UBI) 9–10, AlmaLinux 9–10, and Rocky Linux 9–10.
For the hardware needed to run a node,
see [System Requirements](https://xrpl.org/system-requirements.html).
## Release channels
Packages are published to four channels:
+22 -3
View File
@@ -86,12 +86,15 @@ public:
requires std::convertible_to<TT*, T*>
SharedIntrusive(SharedIntrusive<TT> const& rhs);
SharedIntrusive(SharedIntrusive&& rhs);
// noexcept so move_if_noexcept moves rather than copies, which NodePathStack's path_ vector
// relies on when it relocates. The body is a std::exchange on a raw pointer, which is what
// makes the noexcept honest.
SharedIntrusive(SharedIntrusive&& rhs) noexcept;
template <class TT>
requires std::convertible_to<TT*, T*>
SharedIntrusive(
SharedIntrusive<TT>&& rhs); // NOLINT(cppcoreguidelines-rvalue-reference-param-not-moved)
// NOLINTNEXTLINE(cppcoreguidelines-rvalue-reference-param-not-moved)
SharedIntrusive(SharedIntrusive<TT>&& rhs) noexcept;
SharedIntrusive&
operator=(SharedIntrusive const& rhs);
@@ -529,6 +532,22 @@ staticPointerCast(TT const& v)
return SharedPtr<T>(StaticCastTagSharedIntrusive{}, v);
}
/**
* Statically cast an intrusive pointer, moving out of it.
*
* Spelling the parameter as `SharedIntrusive<TT>&&` binds rvalues only and
* leaves lvalues to the `const&` overload above.
*
* @param v the pointer to cast, left empty afterwards.
* @return a pointer of the requested type to the same object.
*/
template <class T, class TT>
[[nodiscard]] SharedPtr<T>
staticPointerCast(SharedIntrusive<TT>&& v)
{
return SharedPtr<T>(StaticCastTagSharedIntrusive{}, std::move(v));
}
template <class T, class TT>
SharedPtr<T>
dynamicPointerCast(TT const& v)
+2 -2
View File
@@ -47,7 +47,7 @@ SharedIntrusive<T>::SharedIntrusive(SharedIntrusive<TT> const& rhs)
}
template <class T>
SharedIntrusive<T>::SharedIntrusive(SharedIntrusive&& rhs)
SharedIntrusive<T>::SharedIntrusive(SharedIntrusive&& rhs) noexcept
: ptr_{std::move(rhs).unsafeExchange(nullptr)}
{
}
@@ -55,7 +55,7 @@ SharedIntrusive<T>::SharedIntrusive(SharedIntrusive&& rhs)
template <class T>
template <class TT>
requires std::convertible_to<TT*, T*>
SharedIntrusive<T>::SharedIntrusive(SharedIntrusive<TT>&& rhs)
SharedIntrusive<T>::SharedIntrusive(SharedIntrusive<TT>&& rhs) noexcept
: ptr_{std::move(rhs).unsafeExchange(nullptr)}
{
}
+7 -2
View File
@@ -1,10 +1,15 @@
#pragma once
#include <xrpl/basics/TaggedCache.h>
#include <xrpl/basics/base_uint.h>
namespace xrpl {
using KeyCache = TaggedCache<UInt256, int, true>;
/**
* TaggedCache in key-only mode, holding no value per key.
*
* @tparam Key the key type to remember.
*/
template <class Key>
using KeyCache = TaggedCache<Key, int, true>;
} // namespace xrpl
+72
View File
@@ -39,6 +39,31 @@ checkedSub(std::int64_t a, std::int64_t b) noexcept
return a - b;
}
/**
* Multiply two signed 64-bit integers, returning std::nullopt when the exact
* mathematical product is not representable in std::int64_t.
*/
[[nodiscard]] constexpr std::optional<std::int64_t>
checkedMul(std::int64_t a, std::int64_t b) noexcept
{
using L = std::numeric_limits<std::int64_t>;
if (a == 0 || b == 0)
return 0;
if (a > 0)
{
if (b > 0 ? a > L::max() / b : b < L::min() / a)
return std::nullopt;
}
else if (b > 0 ? a < L::min() / b : b < L::max() / a)
{
return std::nullopt;
}
return a * b;
}
static_assert(checkedAdd(0, 0) == 0);
static_assert(checkedAdd(1, -1) == 0);
static_assert(checkedAdd(-5, 2) == -3);
@@ -81,6 +106,53 @@ static_assert(
!checkedSub(std::numeric_limits<std::int64_t>::min(), std::numeric_limits<std::int64_t>::max())
.has_value());
static_assert(checkedMul(0, std::numeric_limits<std::int64_t>::min()) == 0);
static_assert(checkedMul(std::numeric_limits<std::int64_t>::min(), 0) == 0);
static_assert(checkedMul(-1, -1) == 1);
// a > 0, b > 0
static_assert(
checkedMul(std::numeric_limits<std::int64_t>::max(), 1) ==
std::numeric_limits<std::int64_t>::max());
static_assert(
checkedMul(std::numeric_limits<std::int64_t>::max() / 2, 2) ==
std::numeric_limits<std::int64_t>::max() - 1);
static_assert(!checkedMul((std::numeric_limits<std::int64_t>::max() / 2) + 1, 2).has_value());
static_assert(!checkedMul(2, (std::numeric_limits<std::int64_t>::max() / 2) + 1).has_value());
// a > 0, b < 0
static_assert(
checkedMul(std::numeric_limits<std::int64_t>::max(), -1) ==
-std::numeric_limits<std::int64_t>::max());
static_assert(
checkedMul((std::numeric_limits<std::int64_t>::max() / 2) + 1, -2) ==
std::numeric_limits<std::int64_t>::min());
static_assert(!checkedMul((std::numeric_limits<std::int64_t>::max() / 2) + 2, -2).has_value());
static_assert(
checkedMul(1, std::numeric_limits<std::int64_t>::min()) ==
std::numeric_limits<std::int64_t>::min());
static_assert(
checkedMul(2, std::numeric_limits<std::int64_t>::min() / 2) ==
std::numeric_limits<std::int64_t>::min());
static_assert(!checkedMul(2, (std::numeric_limits<std::int64_t>::min() / 2) - 1).has_value());
// a < 0, b > 0
static_assert(
checkedMul(std::numeric_limits<std::int64_t>::min(), 1) ==
std::numeric_limits<std::int64_t>::min());
static_assert(
checkedMul(std::numeric_limits<std::int64_t>::min() / 2, 2) ==
std::numeric_limits<std::int64_t>::min());
static_assert(!checkedMul((std::numeric_limits<std::int64_t>::min() / 2) - 1, 2).has_value());
static_assert(!checkedMul(std::numeric_limits<std::int64_t>::min(), 2).has_value());
// a < 0, b < 0
static_assert(
checkedMul(-(std::numeric_limits<std::int64_t>::max() / 2), -2) ==
std::numeric_limits<std::int64_t>::max() - 1);
static_assert(!checkedMul(std::numeric_limits<std::int64_t>::min() / 2, -2).has_value());
static_assert(
checkedMul(-1, -std::numeric_limits<std::int64_t>::max()) ==
std::numeric_limits<std::int64_t>::max());
static_assert(!checkedMul(std::numeric_limits<std::int64_t>::min(), -1).has_value());
static_assert(!checkedMul(-1, std::numeric_limits<std::int64_t>::min()).has_value());
/**
* Calculate one number divided by another number in percentage.
* The result is rounded up to the next integer, and capped in the range [0,100]
+34 -3
View File
@@ -153,6 +153,24 @@ private:
Policy policy,
Callback&& replaceCallback = nullptr);
/**
* canonicalizeImpl variant for callers that already hold `mutex_`.
*
* The unnamed `scoped_lock<MutexType> const&` parameter is proof of
* ownership; this overload does not acquire the lock. Use it from other
* methods (e.g. fetchAndModify) that need to compose canonicalize with
* additional work under a single critical section, without relying on
* recursive-mutex re-entry.
*/
template <class Policy, class Callback = std::nullptr_t>
bool
canonicalizeImpl(
std::scoped_lock<MutexType> const&,
key_type const& key,
CanonicalizeClientPointerType<Policy> data,
Policy policy,
Callback&& replaceCallback = nullptr);
public:
/**
* Replace aliased objects with originals.
@@ -239,9 +257,6 @@ public:
bool
retrieve(key_type const& key, T& data);
MutexType&
peekMutex();
std::vector<key_type>
getKeys() const;
@@ -263,6 +278,22 @@ public:
fetch(key_type const& digest, Handler const& h);
// End CachedSLEs functions.
/**
* Fetch or create an entry and execute a callback while holding the lock.
*
* The entry for the given key is fetched from the cache or created if it
* doesn't exist. The callback is then invoked with a reference to the
* entry while the cache mutex is still held, allowing safe modification
* of the cached object.
*
* @param key The key to fetch or create
* @param callback Function to call with the entry under lock.
* Signature: void(T&)
*/
template <class Callback>
void
fetchAndModify(key_type const& key, Callback&& callback);
private:
SharedPointerType
initialFetch(key_type const& key, std::scoped_lock<MutexType> const& l);
+67 -18
View File
@@ -4,6 +4,7 @@
#include <xrpl/basics/Log.h> // IWYU pragma: keep
#include <xrpl/basics/TaggedCache.h>
#include <xrpl/basics/scope.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <algorithm>
@@ -335,6 +336,29 @@ template <class Policy, class Callback>
inline bool
TaggedCache<Key, T, IsKeyCache, SharedWeakUnionPointer, SharedPointerType, Hash, KeyEqual, Mutex>::
canonicalizeImpl(
key_type const& key,
CanonicalizeClientPointerType<Policy> data,
Policy policy,
Callback&& replaceCallback)
{
std::scoped_lock<MutexType> const lock(mutex_);
return canonicalizeImpl(lock, key, data, policy, std::forward<Callback>(replaceCallback));
}
template <
class Key,
class T,
bool IsKeyCache,
class SharedWeakUnionPointer,
class SharedPointerType,
class Hash,
class KeyEqual,
class Mutex>
template <class Policy, class Callback>
inline bool
TaggedCache<Key, T, IsKeyCache, SharedWeakUnionPointer, SharedPointerType, Hash, KeyEqual, Mutex>::
canonicalizeImpl(
std::scoped_lock<MutexType> const&,
key_type const& key,
CanonicalizeClientPointerType<Policy> data,
[[maybe_unused]] Policy policy,
@@ -354,8 +378,6 @@ TaggedCache<Key, T, IsKeyCache, SharedWeakUnionPointer, SharedPointerType, Hash,
// passing a const argument is a compile error.
constexpr bool replaceCached = std::is_same_v<Policy, detail::ReplaceCached>;
std::scoped_lock const lock(mutex_);
auto const cit = cache_.find(key);
if (cit == cache_.end())
@@ -572,22 +594,6 @@ TaggedCache<Key, T, IsKeyCache, SharedWeakUnionPointer, SharedPointerType, Hash,
return true;
}
template <
class Key,
class T,
bool IsKeyCache,
class SharedWeakUnionPointer,
class SharedPointerType,
class Hash,
class KeyEqual,
class Mutex>
inline auto
TaggedCache<Key, T, IsKeyCache, SharedWeakUnionPointer, SharedPointerType, Hash, KeyEqual, Mutex>::
peekMutex() -> MutexType&
{
return mutex_;
}
template <
class Key,
class T,
@@ -700,6 +706,49 @@ TaggedCache<Key, T, IsKeyCache, SharedWeakUnionPointer, SharedPointerType, Hash,
}
// End CachedSLEs functions.
template <
class Key,
class T,
bool IsKeyCache,
class SharedWeakUnionPointer,
class SharedPointerType,
class Hash,
class KeyEqual,
class Mutex>
template <class Callback>
inline void
TaggedCache<Key, T, IsKeyCache, SharedWeakUnionPointer, SharedPointerType, Hash, KeyEqual, Mutex>::
fetchAndModify(key_type const& key, Callback&& callback)
{
static_assert(
!IsKeyCache, "fetchAndModify is only supported for value caches, not key-only caches");
static_assert(
std::is_same_v<std::shared_ptr<T>, SharedPointerType> ||
std::is_same_v<intr_ptr::SharedPtr<T>, SharedPointerType>,
"fetchAndModify requires std::shared_ptr<T> or intr_ptr::SharedPtr<T>");
std::scoped_lock<MutexType> const lock(mutex_);
// Same pointer-type dispatch as insert(): the placeholder must have exactly
// SharedPointerType so canonicalizeImpl can write the cached pointer back.
SharedPointerType entry = [] {
if constexpr (std::is_same_v<std::shared_ptr<T>, SharedPointerType>)
{
return std::make_shared<T>();
}
else
{
return intr_ptr::makeShared<T>();
}
}();
canonicalizeImpl(lock, key, entry, detail::ReplaceClient{});
ALWAYS(
entry != nullptr, "xrpl::TaggedCache::fetchAndModify : entry present after canonicalize");
callback(*entry);
}
template <
class Key,
class T,
+3
View File
@@ -94,6 +94,7 @@ struct Keys
static constexpr auto kBbtOptions = "bbt_options";
static constexpr auto kBgThreads = "bg_threads";
static constexpr auto kBlockSize = "block_size";
static constexpr auto kBytecodeSizeLimit = "bytecode_size_limit";
static constexpr auto kCacheAge = "cache_age";
static constexpr auto kCacheMb = "cache_mb";
static constexpr auto kCacheSize = "cache_size";
@@ -108,6 +109,8 @@ struct Keys
static constexpr auto kFileSizeMult = "file_size_mult";
static constexpr auto kFilterBits = "filter_bits";
static constexpr auto kFilterFull = "filter_full";
static constexpr auto kGasLimit = "gas_limit";
static constexpr auto kGasPrice = "gas_price";
static constexpr auto kHardSet = "hard_set";
static constexpr auto kHighThreads = "high_threads";
static constexpr auto kHoldTime = "hold_time";
+86 -15
View File
@@ -105,6 +105,16 @@ public:
std::vector<UInt256> const& amendments,
Family& family);
/**
* Create a ledger from a header whose maps are filled in afterwards.
*
* Both maps start Synching against the hashes the header carries, which are
* input rather than derived, so setImmutable() leaves them alone.
*
* @param info The header to build from.
* @param rules The rules in force.
* @param family The SHAMap family the maps belong to.
*/
Ledger(LedgerHeader const& info, Rules rules, Family& family);
/**
@@ -257,38 +267,74 @@ public:
header_.validated = true;
}
void
/**
* Mark this ledger as accepted and attempt to make it immutable.
*
* The close-time fields are recorded first, since the ledger hash covers
* them.
*
* @param closeTime The consensus-agreed close time.
* @param closeResolution The close time resolution.
* @param correctCloseTime Whether consensus agreed on the close time. If
* false, kSLcfNoConsensusTime is recorded in closeFlags instead.
* @return What setImmutable() returned.
*/
[[nodiscard]] bool
setAccepted(
NetClock::time_point closeTime,
NetClock::duration closeResolution,
bool correctCloseTime);
void
/**
* Mark this ledger as immutable, so it can no longer be modified.
*
* Only a map syncing against hashes from outside can be found unsound (see
* SHAMap::addKnownNode), so a caller whose ledger was built or loaded
* locally treats a false return as a broken internal invariant.
*
* @param rehash Whether to recompute the ledger hash from the header
* fields. The transaction and account hashes are recomputed from the
* maps too, but only the first time and only if the header did not
* supply them.
* @return false if either map is Invalid, leaving the immutable flag as it
* was and the header untouched. The ledger must then be discarded
* rather than retried, since one map may already be immutable.
*/
[[nodiscard]] bool
setImmutable(bool rehash = true);
/**
* @return Whether both maps have been settled, so the ledger can no longer
* change.
*/
bool
isImmutable() const
{
return immutable_;
}
/* Mark this ledger as "should be full".
/**
* @return Whether both maps can still be the maps the header names. See
* SHAMap::isValid().
*/
[[nodiscard]] bool
mapsValid() const
{
return txMap_.isValid() && stateMap_.isValid();
}
"Full" is metadata property of the ledger, it indicates
that the local server wants all the corresponding nodes
in durable storage.
This is marked `const` because it reflects metadata
and not data that is in common with other nodes on the
network.
*/
/**
* Mark this ledger as "should be full", indicating that the local server
* wants all the corresponding nodes in durable storage.
*
* Const because it reflects metadata, not data this ledger shares with
* other nodes on the network.
*/
void
setFull() const
{
txMap_.setFull();
txMap_.setLedgerSeq(header_.seq);
stateMap_.setFull();
stateMap_.setLedgerSeq(header_.seq);
txMap_.setFull(header_.seq);
stateMap_.setFull(header_.seq);
}
void
@@ -418,8 +464,33 @@ private:
static std::pair<std::shared_ptr<STTx const>, std::shared_ptr<STObject const>>
deserializeTxPlusMeta(SHAMapItem const& item);
/**
* Make both maps immutable.
*
* Each map is settled on its own, so neither is left mid-sync because the
* other refused.
*
* @return Whether both maps are immutable.
*/
[[nodiscard]] bool
setMapsImmutable()
{
bool const txImmutable = txMap_.setImmutable();
bool const stateImmutable = stateMap_.setImmutable();
return txImmutable && stateImmutable;
}
bool immutable_;
/**
* Whether the header supplied the transaction and account hashes, so they
* must not be derived from the maps.
*
* True only for a ledger built from a header, which stays verified against
* the hash it was asked for. Fixed at construction, unlike immutable_.
*/
bool const mapHashesFromHeader_ = false;
// A SHAMap containing the transactions associated with this ledger.
SHAMap mutable txMap_;
+9 -1
View File
@@ -265,6 +265,13 @@ canWithdraw(
[[nodiscard]] TER
canWithdraw(ReadView const& view, STTx const& tx);
/**
* Pays out a withdrawal from a vault or loan broker pseudo-account.
*
* `sourceAmount` leaves `sourceAcct` and `destinationAmount` reaches `dstAcct`.
* The two differ only when the withdrawal carries a transfer fee; the
* difference is the fee and is settled through the issuer.
*/
[[nodiscard]] TER
doWithdraw(
ApplyViewContext ctx,
@@ -272,7 +279,8 @@ doWithdraw(
AccountID const& dstAcct,
AccountID const& sourceAcct,
XRPAmount priorBalance,
STAmount const& amount,
STAmount const& destinationAmount,
STAmount const& sourceAmount,
beast::Journal j);
/**
+81 -160
View File
@@ -2,7 +2,6 @@
#include <xrpl/basics/Log.h>
#include <xrpl/basics/Number.h>
#include <xrpl/basics/contract.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/ledger/ApplyView.h>
@@ -29,7 +28,6 @@
#include <expected>
#include <functional>
#include <optional>
#include <stdexcept>
#include <tuple>
#include <utility>
@@ -313,11 +311,11 @@ getAMMOfferStartWithTakerPays(
* is equal to LOB quality (in this case AMM offer quality is
* better than LOB quality) or AMM offer is equal to LOB quality
* (in this case SPQ is better than LOB quality).
* Pre-amendment code calculates takerPays first. If takerGets is the
* economically coarser integral side, it is rounded down, which results in
* worse offer quality than LOB quality, and the offer might fail to generate.
* Post-amendment code calculates the economically coarser integral offer side
* first. The result is rounded down, which makes the offer quality better.
* The economically coarser integral offer side is calculated first. The
* result is rounded down, which makes the offer quality better. Calculating
* takerPays first instead would round down takerGets when that is the coarser
* side, giving worse quality than LOB quality, and the offer might fail to
* generate.
* It might not be possible to match either SPQ or AMM offer to LOB
* quality. This generally happens at higher fees.
* @param pool AMM pool balances
@@ -331,69 +329,8 @@ changeSpotPriceQuality(
TAmounts<TIn, TOut> const& pool,
Quality const& quality,
std::uint16_t tfee,
Rules const& rules,
beast::Journal j)
{
if (!rules.enabled(fixAMMv1_1))
{
// Finds takerPays (i) and takerGets (o) such that given pool
// composition poolGets(I) and poolPays(O): (O - o) / (I + i) = quality.
// Where takerGets is calculated as the swapAssetIn (see below).
// The above equation produces the quadratic equation:
// i^2*(1-fee) + i*I*(2-fee) + I^2 - I*O/quality,
// which is solved for i, and o is found with swapAssetIn().
auto const f = feeMult(tfee); // 1 - fee
auto const& a = f;
auto const b = pool.in * (1 + f);
Number const c = pool.in * pool.in - pool.in * pool.out * quality.rate();
auto const res = b * b - 4 * a * c;
if (res < 0)
{
return std::nullopt; // LCOV_EXCL_LINE
}
if (auto const nTakerPaysPropose = (-b + root2(res)) / (2 * a); nTakerPaysPropose > 0)
{
auto const nTakerPays = [&] {
// The fee might make the AMM offer quality less than CLOB
// quality. Therefore, AMM offer has to satisfy this constraint:
// o / i >= q. Substituting o with swapAssetIn() gives: i <= O /
// q - I / (1 - fee).
auto const nTakerPaysConstraint = pool.out * quality.rate() - pool.in / f;
if (nTakerPaysPropose > nTakerPaysConstraint)
return nTakerPaysConstraint;
return nTakerPaysPropose;
}();
if (nTakerPays <= 0)
{
JLOG(j.trace()) << "changeSpotPriceQuality calc failed: " << to_string(pool.in)
<< " " << to_string(pool.out) << " " << quality << " " << tfee;
return std::nullopt;
}
auto const takerPays =
toAmount<TIn>(getAsset(pool.in), nTakerPays, Number::RoundingMode::Upward);
// should not fail
auto amounts = TAmounts<TIn, TOut>{takerPays, swapAssetIn(pool, takerPays, tfee)};
if (Quality{amounts} < quality &&
!withinRelativeDistance(Quality{amounts}, quality, Number(1, -7)))
{
JLOG(j.error()) << "changeSpotPriceQuality failed: " << to_string(pool.in) << " "
<< to_string(pool.out) << " "
<< " " << quality << " " << tfee << " " << to_string(amounts.in)
<< " " << to_string(amounts.out);
Throw<std::runtime_error>("changeSpotPriceQuality failed");
}
JLOG(j.trace()) << "changeSpotPriceQuality succeeded: " << to_string(pool.in) << " "
<< to_string(pool.out) << " "
<< " " << quality << " " << tfee << " " << to_string(amounts.in) << " "
<< to_string(amounts.out);
return amounts;
}
JLOG(j.trace()) << "changeSpotPriceQuality calc failed: " << to_string(pool.in) << " "
<< to_string(pool.out) << " " << quality << " " << tfee;
return std::nullopt;
}
auto amounts = [&] {
bool const inIntegral = getAsset(pool.in).integral();
bool const outIntegral = getAsset(pool.out).integral();
@@ -459,57 +396,49 @@ template <typename TIn, typename TOut>
TOut
swapAssetIn(TAmounts<TIn, TOut> const& pool, TIn const& assetIn, std::uint16_t tfee)
{
if (auto const& rules = getCurrentTransactionRules(); rules && rules->enabled(fixAMMv1_1))
{
// set rounding to always favor the amm. Clip to zero.
// calculate:
// pool.out -
// (pool.in * pool.out) / (pool.in + assetIn * feeMult(tfee)),
// and explicitly set the rounding modes
// Favoring the amm means we should:
// minimize:
// pool.out -
// (pool.in * pool.out) / (pool.in + assetIn * feeMult(tfee)),
// maximize:
// (pool.in * pool.out) / (pool.in + assetIn * feeMult(tfee)),
// (pool.in * pool.out)
// minimize:
// (pool.in + assetIn * feeMult(tfee)),
// minimize:
// assetIn * feeMult(tfee)
// feeMult is: (1-fee), fee is tfee/100000
// minimize:
// 1-fee
// maximize:
// fee
SaveNumberRoundMode const _{Number::getround()};
// set rounding to always favor the amm. Clip to zero.
// calculate:
// pool.out -
// (pool.in * pool.out) / (pool.in + assetIn * feeMult(tfee)),
// and explicitly set the rounding modes
// Favoring the amm means we should:
// minimize:
// pool.out -
// (pool.in * pool.out) / (pool.in + assetIn * feeMult(tfee)),
// maximize:
// (pool.in * pool.out) / (pool.in + assetIn * feeMult(tfee)),
// (pool.in * pool.out)
// minimize:
// (pool.in + assetIn * feeMult(tfee)),
// minimize:
// assetIn * feeMult(tfee)
// feeMult is: (1-fee), fee is tfee/100000
// minimize:
// 1-fee
// maximize:
// fee
SaveNumberRoundMode const _{Number::getround()};
Number::setround(Number::RoundingMode::Upward);
auto const numerator = pool.in * pool.out;
auto const fee = getFee(tfee);
Number::setround(Number::RoundingMode::Upward);
auto const numerator = pool.in * pool.out;
auto const fee = getFee(tfee);
Number::setround(Number::RoundingMode::Downward);
auto const denom = pool.in + assetIn * (1 - fee);
Number::setround(Number::RoundingMode::Downward);
auto const denom = pool.in + assetIn * (1 - fee);
if (denom.signum() <= 0)
return toAmount<TOut>(getAsset(pool.out), 0);
if (denom.signum() <= 0)
return toAmount<TOut>(getAsset(pool.out), 0);
Number::setround(Number::RoundingMode::Upward);
auto const ratio = numerator / denom;
Number::setround(Number::RoundingMode::Upward);
auto const ratio = numerator / denom;
Number::setround(Number::RoundingMode::Downward);
auto const swapOut = pool.out - ratio;
Number::setround(Number::RoundingMode::Downward);
auto const swapOut = pool.out - ratio;
if (swapOut.signum() < 0)
return toAmount<TOut>(getAsset(pool.out), 0);
if (swapOut.signum() < 0)
return toAmount<TOut>(getAsset(pool.out), 0);
return toAmount<TOut>(getAsset(pool.out), swapOut, Number::RoundingMode::Downward);
}
return toAmount<TOut>(
getAsset(pool.out),
pool.out - (pool.in * pool.out) / (pool.in + assetIn * feeMult(tfee)),
Number::RoundingMode::Downward);
return toAmount<TOut>(getAsset(pool.out), swapOut, Number::RoundingMode::Downward);
}
/**
@@ -526,57 +455,49 @@ template <typename TIn, typename TOut>
TIn
swapAssetOut(TAmounts<TIn, TOut> const& pool, TOut const& assetOut, std::uint16_t tfee)
{
if (auto const& rules = getCurrentTransactionRules(); rules && rules->enabled(fixAMMv1_1))
// set rounding to always favor the amm. Clip to zero.
// calculate:
// ((pool.in * pool.out) / (pool.out - assetOut) - pool.in) /
// (1-tfee/100000)
// maximize:
// ((pool.in * pool.out) / (pool.out - assetOut) - pool.in)
// maximize:
// (pool.in * pool.out) / (pool.out - assetOut)
// maximize:
// (pool.in * pool.out)
// minimize
// (pool.out - assetOut)
// minimize:
// (1-tfee/100000)
// maximize:
// tfee/100000
SaveNumberRoundMode const _{Number::getround()};
Number::setround(Number::RoundingMode::Upward);
auto const numerator = pool.in * pool.out;
Number::setround(Number::RoundingMode::Downward);
auto const denom = pool.out - assetOut;
if (denom.signum() <= 0)
{
// set rounding to always favor the amm. Clip to zero.
// calculate:
// ((pool.in * pool.out) / (pool.out - assetOut) - pool.in) /
// (1-tfee/100000)
// maximize:
// ((pool.in * pool.out) / (pool.out - assetOut) - pool.in)
// maximize:
// (pool.in * pool.out) / (pool.out - assetOut)
// maximize:
// (pool.in * pool.out)
// minimize
// (pool.out - assetOut)
// minimize:
// (1-tfee/100000)
// maximize:
// tfee/100000
SaveNumberRoundMode const _{Number::getround()};
Number::setround(Number::RoundingMode::Upward);
auto const numerator = pool.in * pool.out;
Number::setround(Number::RoundingMode::Downward);
auto const denom = pool.out - assetOut;
if (denom.signum() <= 0)
{
return toMaxAmount<TIn>(getAsset(pool.in));
}
Number::setround(Number::RoundingMode::Upward);
auto const ratio = numerator / denom;
auto const numerator2 = ratio - pool.in;
auto const fee = getFee(tfee);
Number::setround(Number::RoundingMode::Downward);
auto const feeMult = 1 - fee;
Number::setround(Number::RoundingMode::Upward);
auto const swapIn = numerator2 / feeMult;
if (swapIn.signum() < 0)
return toAmount<TIn>(getAsset(pool.in), 0);
return toAmount<TIn>(getAsset(pool.in), swapIn, Number::RoundingMode::Upward);
return toMaxAmount<TIn>(getAsset(pool.in));
}
return toAmount<TIn>(
getAsset(pool.in),
((pool.in * pool.out) / (pool.out - assetOut) - pool.in) / feeMult(tfee),
Number::RoundingMode::Upward);
Number::setround(Number::RoundingMode::Upward);
auto const ratio = numerator / denom;
auto const numerator2 = ratio - pool.in;
auto const fee = getFee(tfee);
Number::setround(Number::RoundingMode::Downward);
auto const feeMult = 1 - fee;
Number::setround(Number::RoundingMode::Upward);
auto const swapIn = numerator2 / feeMult;
if (swapIn.signum() < 0)
return toAmount<TIn>(getAsset(pool.in), 0);
return toAmount<TIn>(getAsset(pool.in), swapIn, Number::RoundingMode::Upward);
}
/**
+124 -38
View File
@@ -7,10 +7,10 @@
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/helpers/VaultHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Asset.h>
#include <xrpl/protocol/LedgerFormats.h> // IWYU pragma: keep
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/Rules.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
@@ -31,9 +31,9 @@ namespace xrpl {
/**
* Broker cover preclaim precision guard (fixCleanup3_2_0).
*
* Prevents a "silent sub-ULP no-op" where a deposit, withdrawal, or clawback
* amount is so small that it rounds to zero at `sfCoverAvailable`'s scale.
* Without this guard, both the pseudo trust-line and `sfCoverAvailable` would
* Prevents a silent sub-ULP no-op where a deposit, withdrawal, or clawback
* amount is so small that it rounds to zero at sfCoverAvailable's scale.
* Without this guard, both the pseudo trust-line and sfCoverAvailable would
* identically absorb the rounded zero, resulting in a successful transaction
* (tesSUCCESS) where no funds actually moved.
*
@@ -44,8 +44,8 @@ namespace xrpl {
* @param j Journal for logging.
* @param logPrefix Transactor name for log diagnostics.
*
* @return `tecPRECISION_LOSS` if the request rounds to zero at cover scale.
* `tesSUCCESS` if the amendment is disabled or the request is safely supra-ULP.
* @return tecPRECISION_LOSS if the request rounds to zero at cover scale.
* tesSUCCESS if the amendment is disabled or the request is safely supra-ULP.
*/
[[nodiscard]] TER
canApplyToBrokerCover(
@@ -56,6 +56,82 @@ canApplyToBrokerCover(
beast::Journal j,
std::string_view logPrefix);
namespace detail {
/**
* Return a LoanBroker's posterior live cover exponent after applying an
* unrounded delta.
*/
[[nodiscard]] int
getPosteriorBrokerCoverScale(SLE::ConstRef vault, SLE::ConstRef broker, Number const& delta);
/**
* Round a cover outflow delta (cover withdraw, cover clawback, the
* broker-side decrease of a default's cover credit) at the LoanBroker's
* posterior live exponent. Safe to round the delta directly for an outflow:
* amount on the posterior grid of CoverAvailable always leaves CoverAvailable
* minus amount representable, since the posterior grid is exactly the grid
* CoverAvailable itself will canonicalize to after the subtraction.
*/
[[nodiscard]] STAmount
roundToPosteriorBrokerCoverScale(
SLE::ConstRef vault,
SLE::ConstRef broker,
STAmount const& delta,
Number::RoundingMode roundingMode);
} // namespace detail
/**
* Round a cover debit (cover withdraw, cover clawback, the broker-side
* decrease of a default's cover credit): amount is the non-negative
* magnitude leaving CoverAvailable. Negates so the posterior is computed for
* CoverAvailable minus amount, then negates the result back to a
* non-negative magnitude, per roundToPosteriorBrokerCoverScale's outflow
* rationale above. Magnitude in, magnitude out -- unlike the vault-side
* outflow dispatcher (clampVaultOutflow), which takes and returns a negative
* delta; callers on each side already hold the value in that side's native
* form.
*/
[[nodiscard]] STAmount
debitToPosteriorBrokerCoverScale(
SLE::ConstRef vault,
SLE::ConstRef broker,
STAmount const& amount,
Number::RoundingMode roundingMode);
/**
* Round a cover inflow (cover deposit, LoanPay's fee redirect into cover)
* into CoverAvailable by flooring the sum rather than the delta:
* credit = floor16(CoverAvailable + raw) - CoverAvailable, never finer
* than the Vault's base exponent (-Scale). See
* creditToPosteriorAvailableScale's doc for why the sum, not the delta,
* must be floored; the same reasoning applies to CoverAvailable.
*/
[[nodiscard]] STAmount
creditToPosteriorBrokerCoverScale(
SLE::ConstRef vault,
SLE::ConstRef broker,
Number const& raw,
Number::RoundingMode roundingMode);
/**
* Check whether amount is an admissible optional cover inflow.
*
* Legacy and CashBasis Vaults always succeed. A LoanBroker attached to a
* FixedPrecision Vault must remain at the Vault's base scale after applying
* the rounded amount, and its posterior CoverAvailable must stay within the
* Open zone.
*
* LoanPay's fee redirect into cover is a mandatory inflow and does not call
* this check, so it can push CoverAvailable past the Open zone limit this
* check enforces. Once that happens, every subsequent LoanBrokerCoverDeposit
* returns tecLIMIT_EXCEEDED until the owner withdraws cover back under the
* limit; LoanBrokerCoverWithdraw and LoanBrokerCoverClawback are unaffected.
*/
[[nodiscard]] TER
checkOptionalBrokerCoverInflow(SLE::ConstRef vault, SLE::ConstRef broker, STAmount const& amount);
// Lending protocol has dependencies, so capture them here.
bool
checkLendingProtocolDependencies(Rules const& rules, STTx const& tx);
@@ -64,13 +140,13 @@ checkLendingProtocolDependencies(Rules const& rules, STTx const& tx);
* The accounts and asset that LoanManage::defaultLoan's fixCleanup3_4_0
* freeze/lock exemption applies to.
*
* `defaultLoan` moves funds from the LoanBroker pseudo-account to the Vault
* pseudo-account via `accountSend`. Since neither is the vault asset's
* defaultLoan moves funds from the LoanBroker pseudo-account to the Vault
* pseudo-account via accountSend. Since neither is the vault asset's
* issuer, this is a third-party transfer that transits through the issuer in
* two hops (broker -> issuer, issuer -> vault; see
* `directSendNoLimitIOU`/`directSendNoLimitMPT`), so the exemption must cover
* directSendNoLimitIOU/directSendNoLimitMPT), so the exemption must cover
* both the issuer/broker and issuer/vault pairs, not a direct broker/vault
* pair. `asset` scopes it further to the vault's own currency/MPT issuance,
* pair. asset scopes it further to the vault's own currency/MPT issuance,
* so an unrelated one the same accounts happen to hold is still protected.
*/
struct LoanDefaultFreezeExemptAccounts
@@ -88,10 +164,10 @@ struct LoanDefaultFreezeExemptAccounts
* @param view Ledger view used to resolve the Loan -> LoanBroker -> Vault
* chain.
* @param tx The transaction under invariant review.
* @return The exempt accounts and asset if `tx` is a `ttLOAN_MANAGE`
* transaction with the `tfLoanDefault` flag set, `fixCleanup3_4_0` is
* @return The exempt accounts and asset if tx is a ttLOAN_MANAGE
* transaction with the tfLoanDefault flag set, fixCleanup3_4_0 is
* enabled, and the loan/broker/vault objects it references can all be
* resolved; `std::nullopt` otherwise.
* resolved; std::nullopt otherwise.
*/
[[nodiscard]] std::optional<LoanDefaultFreezeExemptAccounts>
getLoanDefaultFreezeExemptAccounts(ReadView const& view, STTx const& tx);
@@ -254,28 +330,38 @@ adjustImpreciseNumber(
value = 0;
}
inline int
getAssetsTotalScale(SLE::ConstRef vaultSle)
{
if (!vaultSle)
return Number::kMinExponent - 1; // LCOV_EXCL_LINE
return scale(vaultSle->at(sfAssetsTotal), vaultSle->at(sfAsset));
}
/**
* Apply a signed delta to a LoanBroker's DebtTotal: on FixedPrecision Vaults,
* add the exact delta (DebtTotal moves by exactly the same amount as the
* Vault's AssetsDeployed, never re-rounded); on Legacy/CashBasis Vaults,
* round through adjustImpreciseNumber at vaultScale. Shared by LoanSet
* (origination) and LoanPay (payment, where delta is negative).
*
* @param brokerSle The LoanBroker whose DebtTotal is adjusted.
* @param vaultSle The LoanBroker's Vault.
* @param delta The signed change to DebtTotal.
* @param vaultScale The Vault scale the caller captured before changing
* AssetsTotal. Legacy/CashBasis round DebtTotal at that scale, not at
* the scale after this transaction's AssetsTotal change. Unused on
* FixedPrecision Vaults.
*/
void
adjustBrokerDebtTotal(
SLE::Ref brokerSle,
SLE::ConstRef vaultSle,
Number const& delta,
int vaultScale);
// Compute the minimum required broker cover, rounded consistently.
// DebtTotal is a broker-level aggregate maintained at vault scale, so the
// rounding must also use vault scale — never an individual loan's scale.
inline Number
minimumBrokerCover(Number const& debtTotal, TenthBips32 coverRateMinimum, SLE::ConstRef vaultSle)
{
XRPL_ASSERT(
vaultSle && vaultSle->getType() == ltVAULT, "xrpl::minimumBrokerCover : valid Vault sle");
NumberRoundModeGuard const mg(Number::RoundingMode::Upward);
return roundToAsset(
vaultSle->at(sfAsset),
tenthBipsOfValue(debtTotal, coverRateMinimum),
getAssetsTotalScale(vaultSle));
}
/**
* Minimum required broker cover, rounded up.
*
* DebtTotal is a broker-level aggregate, never rounded at an individual
* loan's scale. Legacy and CashBasis Vaults round at the live AssetsTotal
* exponent. FixedPrecision Vaults round at the Vault's base exponent
* (-Scale, or 0 for integral assets).
*/
Number
minimumBrokerCover(Number const& debtTotal, TenthBips32 coverRateMinimum, SLE::ConstRef vaultSle);
TER
checkLoanGuards(
@@ -379,10 +465,10 @@ loanPaymentDeltas(LoanPaymentParts const& parts);
} // namespace cash_basis
// Public dispatchers: pick cash_basis:: if featureLendingProtocolV1_1 is
// enabled AND the Vault's LEVersion (VaultHelpers::getVaultVersion) is
// VaultVersion::CashBasis, else instant_recognition::. These are the only entry points
// transactors call.
// Public dispatchers: pick cash_basis:: if the Vault's LEVersion
// (VaultHelpers::getVaultVersion) is CashBasis or later (CashBasis or
// FixedPrecision), else instant_recognition::. These are the only entry
// points transactors call.
AccountingDeltas
loanOriginationDeltas(
SLE::ConstRef vaultSle,
+25 -4
View File
@@ -312,12 +312,22 @@ checkCreateMPT(
//
//------------------------------------------------------------------------------
// MaximumAmount doesn't exceed 2**63-1
/**
* MaximumAmount doesn't exceed 2**63-1
*/
std::int64_t
maxMPTAmount(SLE const& sleIssuance);
// OutstandingAmount may overflow and available amount might be negative.
// But available amount is always <= |MaximumAmount - OutstandingAmount|.
/**
* std::nullopt if the issuance doesn't exist.
*/
std::optional<std::int64_t>
maxMPTAmount(ReadView const& view, MPTID const& mptID);
/**
* OutstandingAmount may overflow and available amount might be negative.
* But available amount is always <= |MaximumAmount - OutstandingAmount|.
*/
std::int64_t
availableMPTAmount(SLE const& sleIssuance);
@@ -331,7 +341,12 @@ availableMPTAmount(ReadView const& view, MPTID const& mptID);
* directSendNoFee transactions that bypass the payment engine.
* 2. **accountSend & Payment Engine (Overflow: Yes):** A temporary overflow
* check when `OutstandingAmount > UINT64_MAX`. This higher threshold is used
* for `accountSend` and payments processed via the payment engine.
* for `accountSend` and payments processed via the payment engine, whose
* reverse pass may issue before it redeems.
*
* In both cases a single `sendAmount > MaximumAmount` is rejected. Such a send
* can never settle: redemptions within the transaction are bounded by the
* outstanding amount, so the final OutstandingAmount would exceed the cap.
*/
bool
isMPTOverflow(
@@ -353,6 +368,12 @@ issuerFundsToSelfIssue(ReadView const& view, MPTIssue const& issue);
/**
* Facilitate tracking of MPT sold by an issuer owning MPT sell offer.
* See ApplyView::issuerSelfDebitHookMPT().
*
* `amount` may be 0. BookStep removes an offer whose fee-adjusted funded
* output is 0, but other clips (for example MaximumAmount via limitStepIn)
* can still consume a zero slice of a funded remainder. The hook must
* accept 0: first call still snapshots origBalance; a zero debit adds
* nothing to selfDebit.
*/
void
issuerSelfDebitHookMPT(ApplyView& view, MPTIssue const& issue, std::uint64_t amount);
+108 -21
View File
@@ -1,10 +1,17 @@
#pragma once
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFormats.h>
#include <cstdint>
#include <optional>
namespace xrpl::proposal {
@@ -30,31 +37,22 @@ proposalOwnerCount(STObject const& proposedTx)
: kProposalOwnerCount;
}
/**
* Whether the proposed transaction is itself a proposal transaction, which
* would nest one proposal inside another.
*
* TODO: cover ttTRANSACTION_PROPOSAL_SIGN and ttTRANSACTION_PROPOSAL_CANCEL
* once those transactions exist.
*/
inline bool
isProposalTx(STObject const& proposedTx)
{
return proposedTx.getFieldU16(sfTransactionType) == ttTRANSACTION_PROPOSAL_CREATE;
}
/**
* Whether the proposed transaction is independently submittable through the
* ordinary multi-sign path: not a nested proposal, not a pseudo-transaction,
* not itself flagged as someone else's inner batch transaction, and — if it
* is a Batch — none of its own inner transactions is a nested proposal or a
* pseudo-transaction either. A Batch inner transaction cannot itself be
* pseudo (preflight0 rejects the pseudo/tfInnerBatchTxn combination
* generically), but that guard lives outside this feature, so it is checked
* again here rather than relied upon.
* ordinary multi-sign path: not a proposal transaction itself, not a
* pseudo-transaction, not itself flagged as someone else's inner batch
* transaction, and — if it is a Batch — every one of its own inner
* transactions carries a TransactionType and is neither a proposal, a
* pseudo-transaction, nor a nested Batch. A Batch inner transaction cannot
* lack a TransactionType, be pseudo, or be a nested Batch (STTx construction
* rejects a typeless inner and an inner Batch, and preflight0 rejects the
* pseudo/tfInnerBatchTxn combination), but those guards live outside this
* feature, so they are checked again here rather than relied upon.
*
* TODO: cover ttTRANSACTION_PROPOSAL_SIGN once that transaction exists.
*/
bool
isValidProposal(STObject const& proposedTx);
isValidProposalTxnType(STObject const& proposedTx);
/**
* Whether the proposed transaction carries any signature field.
@@ -84,4 +82,93 @@ hasEmptySigningPubKey(STObject const& proposedTx)
proposedTx.getFieldVL(sfSigningPubKey).empty();
}
/**
* Whether the proposal is terminal (On-Chain Cosigner spec §4.5). A terminal proposal
* stops accepting signatures and anyone may delete it. An already signed
* payload can still execute after Expiration, because the Ticket reservation
* compares payloads only; a passed LastLedgerSequence does stop it.
*
* A proposal is terminal when either:
* - its Expiration has passed (the parent ledger closed at or after it), or
* - the proposed transaction carries a LastLedgerSequence and the current
* ledger sequence is strictly greater than it. This mirrors the ordinary
* transactor's tefMAX_LEDGER rule: at view.seq() == LastLedgerSequence the
* payload can still be submitted in the current ledger, so the proposal is
* still live.
*
* @param view The ledger the deciding transaction is being applied to.
* @param expiration The proposal's Expiration field.
* @param proposedTx The proposal's ProposedTransaction field.
*/
bool
isTerminal(
ReadView const& view,
std::optional<std::uint32_t> expiration,
STObject const& proposedTx);
/**
* Delete a TransactionProposal ledger entry.
*
* Removes the entry from its Owner's directory, releases the reserve the
* proposal holds against the Owner, and erases the entry. Shared by every
* deletion path the spec defines (On-Chain Cosigner spec §4.5): automatic cleanup when the
* proposed transaction's TicketSequence is consumed, and the
* TransactionProposalCancel / TransactionProposalSign cleanup paths once
* those transactions exist.
*
* The release goes through decreaseOwnerCountForObject, so it follows the
* sfSponsor an entry created with reserve sponsorship carries: the reserve is
* returned to whoever was holding it, the Owner or that sponsor.
*
* @param view The apply view for making changes
* @param sleProposal The TransactionProposal ledger entry to delete
* @param j Journal for logging
* @return tesSUCCESS, tecINTERNAL for an invalid entry, or tefBAD_LEDGER if
* the ledger contradicts the entry
*/
TER
deleteProposal(ApplyView& view, SLE::pointer const& sleProposal, beast::Journal j);
/**
* Whether tx carries the same payload as a proposal's stored
* ProposedTransaction (On-Chain Cosigner spec §4.2.1).
*
* The payload is the signing payload: the fields the collected signatures
* commit to. Compared is every field except those flagged kNotSigning
* (TxnSignature, Signers, BatchSigners, CounterpartySignature,
* SponsorSignature, Signature, MasterSignature) and SigningPubKey, which is
* stored empty and filled only if the target signs with its own key. So the
* completed transaction matches no matter which mix of collected or
* off-ledger signatures it carries, and any change to a non-signature field
* results in a false-return value.
*
* Signature (PaymentChannelClaim and the XChain attestation transactions)
* is deliberately not compared: it is outside the account's own TxnSignature
* even for an ordinary submission, so a proposal cannot bind it more tightly
* than the signatures it collects do, and those transactors still validate a
* substituted value exactly as they would for an ordinary submission.
* MasterSignature appears in no transaction format.
*
* @param proposedTx The proposal's ProposedTransaction field.
* @param tx The transaction to compare, typically an STTx.
*/
bool
payloadMatches(STObject const& proposedTx, STObject const& tx);
/**
* Whether tx may consume its Ticket.
*
* While a proposal keyed to the Ticket exists, the target's Ticket is
* reserved: only the proposal's own proposed transaction may spend it
* (On-Chain Cosigner spec §4.2.1), so that unrelated target-account activity cannot
* invalidate the proposal while signatures are being collected.
* TransactionProposalCancel is not allowed through: it deletes the
* reservation without consuming the ticket.
*
* @param view The ledger containing the Ticket and any reservation.
* @param tx The transaction attempting to consume that ticket.
*/
bool
canConsumeTicket(ReadView const& view, STTx const& tx);
} // namespace xrpl::proposal
@@ -316,6 +316,20 @@ transferRate(ReadView const& view, Asset const& asset);
[[nodiscard]] Rate
transferRate(ReadView const& view, STAmount const& amount);
/**
* Returns the amount a sender must part with for `amount` to reach the
* receiver once the issuer takes the transfer fee. Rounds up.
*/
[[nodiscard]] STAmount
addTransferFee(STAmount const& amount, Rate const& rate);
/**
* Returns the amount a receiver gets when `amount` leaves the sender and the
* issuer takes the transfer fee. Rounds down.
*/
[[nodiscard]] STAmount
subtractTransferFee(STAmount const& amount, Rate const& rate);
//------------------------------------------------------------------------------
//
// Holding operations (Asset-based dispatchers)
+55 -35
View File
@@ -14,6 +14,7 @@
#include <cstdint>
#include <expected>
#include <optional>
#include <string_view>
namespace xrpl {
@@ -80,6 +81,21 @@ struct VaultBalanceChange
[[nodiscard]] TER
adjustVaultBalances(SLE::Ref vault, VaultBalanceChange const& change, beast::Journal j);
/**
* @brief Apply a signed cash movement to a Vault of any version. FixedPrecision
* Vaults go through adjustVaultBalances; Legacy and CashBasis Vaults add the
* amount to both AssetsTotal and AssetsAvailable.
*
* @param vault The vault SLE.
* @param delta Signed change to AssetsAvailable; the exact amount moved on the
* Vault pseudo-account.
* @param j Journal passed through to adjustVaultBalances.
*
* @return tesSUCCESS, or the adjustVaultBalances error for FixedPrecision Vaults.
*/
[[nodiscard]] TER
adjustVaultCash(SLE::Ref vault, STAmount const& delta, beast::Journal j);
/**
* Return the Vault's current live exponent.
*
@@ -148,18 +164,20 @@ posteriorAssetScale(
/**
* Shared core of creditToPosteriorAvailableScale and
* creditToPosteriorBrokerCoverScale: floors reference + raw at scale and
* returns the difference from reference, so the delta applied is exactly
* what moves reference onto the floored sum. See
* creditToPosteriorAvailableScale's doc for why the sum, not raw alone,
* must be floored. raw is the exact amount; it is not rounded on its own
* before being added to reference.
* creditToPosteriorBrokerCoverScale: floors reference + raw at the sum's
* own live scale (never finer than baseScale) and returns the difference
* from reference, so the delta applied is exactly what moves reference onto
* the floored sum. See creditToPosteriorAvailableScale's doc for why the
* sum, not raw alone, must be floored. raw is the exact amount; it is not
* rounded on its own before being added to reference. The sum and its scale
* are computed once, in roundingMode, so a Downward credit is always
* between zero and raw.
*/
[[nodiscard]] STAmount
creditToPosteriorScale(
Asset const& asset,
Number const& reference,
int atScale,
int baseScale,
Number const& raw,
Number::RoundingMode roundingMode);
@@ -333,6 +351,18 @@ vaultBaseScale(Asset const& asset, std::uint8_t scale);
[[nodiscard]] bool
isOnVaultBaseGrid(Asset const& asset, Number const& value, int baseScale);
/**
* Checks that a requested AssetsMaximum is exactly representable on the
* Vault's base grid, otherwise associateAsset would silently round the cap
* the owner asked for. Used by VaultSet::preclaim on an existing Vault.
*
* @param vault The vault ledger entry.
* @param amount The requested AssetsMaximum.
* @return tesSUCCESS, or tecPRECISION_LOSS if amount is not representable.
*/
[[nodiscard]] TER
checkAssetsMaximum(SLE::ConstRef vault, Number const& amount);
/**
* Controls whether to truncate shares instead of rounding.
*/
@@ -360,35 +390,24 @@ enum class WaiveUnrealizedLoss : bool { No = false, Yes = true };
assetsTotalForWithdrawal(SLE::ConstRef vault, WaiveUnrealizedLoss waive);
/**
* Returns true if debiting amount from total (the current value of a
* vault's sfAssetsTotal or sfAssetsAvailable) would canonicalize to the
* same STAmount value. This happens when amount is non-zero but too small
* to change the stored total at STAmount's precision. Shares would still
* move, so the ValidVault invariant would fail after apply; callers use
* this to reject the transaction upfront instead.
*
* @param asset The vault's underlying asset, used to canonicalize both
* sides the same way the ledger will when the field is stored.
* @param total The field's current value.
* @param amount The amount to debit. Zero always returns false; that case
* is rejected separately.
*/
[[nodiscard]] bool
debitIsNonZeroDust(Asset const& asset, Number const& total, Number const& amount);
/**
* Returns the reference balance a cash outflow's dust check
* (debitIsNonZeroDust) should compare against: FixedPrecision Vaults use
* AssetsAvailable, the balance the cash actually leaves; Legacy/CashBasis
* Vaults use the given AssetsTotal. Shared by VaultWithdraw and
* VaultClawback.
* Reject a cash outflow that is dust (see debitIsNonZeroDust) or whose check
* overflows Number, which is easy to hit with a large Scale.
*
* @param vault The vault SLE.
* @param assetsTotal The vault's current AssetsTotal, as already read by the
* caller.
* @param amount The amount to debit.
* @param j Journal for the debug log.
* @param name Transaction name used as the log prefix.
*
* @return tesSUCCESS; tecPRECISION_LOSS if amount is dust; tecPATH_DRY if the
* check overflows. Overflow means the transaction cannot apply, but
* ledger state is still consistent.
*/
[[nodiscard]] Number
vaultDebitDustReference(SLE::ConstRef vault, Number const& assetsTotal);
[[nodiscard]] TER
checkDebitNotDust(
SLE::ConstRef vault,
Number const& amount,
beast::Journal j,
std::string_view name);
/**
* From the perspective of a vault, return the number of shares to demand from
@@ -483,8 +502,9 @@ decodeVaultVersion(std::optional<std::uint8_t> leVersion);
*
* @param rules The active ledger rules.
*
* @return VaultVersion::FixedPrecision once featureLendingProtocolV1_2,
* fixCleanup3_2_0 and fixCleanup3_4_0 are all enabled; VaultVersion::CashBasis
* @return VaultVersion::FixedPrecision once featureLendingProtocolV1_1,
* featureLendingProtocolV1_2 and fixCleanup3_4_0 are all enabled;
* VaultVersion::CashBasis
* once featureLendingProtocolV1_1 is enabled; VaultVersion::Legacy otherwise.
*/
[[nodiscard]] VaultVersion
+10 -5
View File
@@ -348,16 +348,19 @@ isAuditorMirrorCurrent(SLE const& issuance, SLE const& mptoken);
areMirrorsCurrent(SLE const& issuance, SLE const& mptoken);
/**
* @brief Set the holder's issuer mirror epoch to match the issuance's current issuer key epoch.
* @brief Set the holder's issuer mirror epoch, and the issuer mirror key, to match the issuance's
* current key.
*
* Call this after writing the issuer mirror ciphertext under the issuance's
* currently registered issuer key, so that the mirror reads as current afterwards.
* Also records sfIssuerMirrorEncryptionKey as the issuance's current issuer key
* (both fields stay absent at epoch 0, where the original issuer key applies).
*
* @param issuance The MPTokenIssuance ledger object.
* @param mptoken The holder's MPToken ledger entry to update.
*/
void
setIssuerMirrorEpoch(SLE const& issuance, SLE& mptoken);
setIssuerMirrorCurrent(SLE const& issuance, SLE& mptoken);
/**
* @brief Set the holder's auditor mirror epoch to match the issuance's current auditor key epoch.
@@ -372,16 +375,18 @@ void
setAuditorMirrorEpoch(SLE const& issuance, SLE& mptoken);
/**
* @brief Set the holder's MPToken mirror epochs to match the issuance's current key epochs.
* @brief Set the holder's MPToken mirror epochs, and the issuer mirror key, to match the issuance's
* current keys.
*
* Call this after writing mirror ciphertexts under the issuance's currently
* registered keys, so that the mirrors read as current afterwards.
* registered keys, so that the mirrors read as current afterwards. Also records
* sfIssuerMirrorEncryptionKey via setIssuerMirrorCurrent.
*
* @param issuance The MPTokenIssuance ledger object.
* @param mptoken The holder's MPToken ledger entry to update.
*/
void
setMirrorEpochs(SLE const& issuance, SLE& mptoken);
setMirrorsCurrent(SLE const& issuance, SLE& mptoken);
/**
* @brief Verifies revealed amount encryptions for all recipients.
+54 -1
View File
@@ -10,6 +10,25 @@ namespace xrpl {
// This was the reference fee units used in the old fee calculation.
inline constexpr std::uint32_t kFeeUnitsDeprecated = 10;
// Number of micro-drops in one drop.
inline constexpr std::uint32_t microDropsPerDrop{1'000'000};
/**
* Hard protocol bounds on the Feature Extension fee settings. A voted value
* can never fall outside these, so `preflight`, which has no view, may bound
* against them. Gas and bytecode limits are capped; gas price has a floor
* (zero would make WASM execution effectively free).
*/
inline constexpr std::uint32_t kMaxGasLimit{2'000'000};
inline constexpr std::uint32_t kMaxBytecodeSizeLimit{200'000};
inline constexpr std::uint32_t kMinGasPrice{1};
// The following default values of fee settings will seed into FeeSettings
// and write to the ledger on featureSmartEscrow activation.
inline constexpr std::uint32_t kDefaultGasLimit{1'000'000};
inline constexpr std::uint32_t kDefaultBytecodeSizeLimit{100'000};
inline constexpr std::uint32_t kDefaultGasPrice{1'000'000};
/**
* Reflects the fee settings for a particular ledger.
*
@@ -33,13 +52,47 @@ struct Fees
*/
XRPAmount increment{0};
/**
* @brief Gas limit for Feature Extensions (instructions).
*
* This and the one below default to the protocol values rather than 0,
* because an explicit 0 is the voted kill switch.
*/
std::uint32_t gasLimit{kDefaultGasLimit};
/**
* @brief Bytecode size limit for Feature Extensions (bytes).
*/
std::uint32_t bytecodeSizeLimit{kDefaultBytecodeSizeLimit};
/**
* @brief Price of WASM gas (micro-drops).
*/
std::uint32_t gasPrice{kDefaultGasPrice};
explicit Fees() = default;
Fees(Fees const&) = default;
Fees&
operator=(Fees const&) = default;
Fees(XRPAmount base, XRPAmount reserve, XRPAmount increment)
: base(base), reserve(reserve), increment(increment)
: base{base}, reserve{reserve}, increment{increment}
{
}
Fees(
XRPAmount base,
XRPAmount reserve,
XRPAmount increment,
std::uint32_t gasLimit,
std::uint32_t bytecodeSizeLimit,
std::uint32_t gasPrice)
: base{base}
, reserve{reserve}
, increment{increment}
, gasLimit{gasLimit}
, bytecodeSizeLimit{bytecodeSizeLimit}
, gasPrice{gasPrice}
{
}
+2 -2
View File
@@ -317,12 +317,12 @@ constexpr std::uint8_t kVaultDefaultIouScale = 6;
* Chosen so 1 IOU can always convert to shares:
* 10^19 > maxMPTokenAmount (2^64-1) > 10^18.
*/
constexpr std::uint8_t kVaultMaximumIouScale = 18;
constexpr std::uint8_t kLegacyVaultMaximumIouScale = 18;
/**
* Maximum Scale for a Vault created under featureLendingProtocolV1_2.
*/
constexpr std::uint8_t kVaultMaximumFixedPrecisionIouScale = 10;
constexpr std::uint8_t kFixedPrecisionVaultMaximumIouScale = 10;
/**
* Vault ledger-entry schema versions, persisted as sfLEVersion.
+10
View File
@@ -696,6 +696,16 @@ divRound(STAmount const& v1, STAmount const& v2, Asset const& asset, bool roundU
STAmount
divRoundStrict(STAmount const& v1, STAmount const& v2, Asset const& asset, bool roundUp);
/**
* Returns amt * num / den, rounded in the requested direction.
*
* Dispatches to the XRPAmount, IOUAmount or MPTAmount overload. XRP and MPT
* results are exact; an IOU result is rounded to the 16-digit mantissa in the
* requested direction. Only Upward and Downward are supported.
*/
[[nodiscard]] STAmount
mulRatio(STAmount const& amt, std::uint32_t num, std::uint32_t den, Number::RoundingMode rounding);
// Someone is offering X for Y, what is the rate?
// Rate: smaller is better, the taker wants the most out: in/out
// VFALCO TODO Return a Quality object
+2
View File
@@ -231,6 +231,8 @@ enum TERcodes : TERUnderlyingType {
terNO_DELEGATE_PERMISSION, // Delegate does not have permission
terLOCKED, // MPT is locked
terNO_PERMISSION, // No permission but retry
terTICKET_RESERVED, // Ticket is reserved and cannot be consumed
// until the reservation is released
};
//------------------------------------------------------------------------------
+13 -20
View File
@@ -1,5 +1,6 @@
#pragma once
#include <xrpl/basics/MathUtilities.h>
#include <xrpl/basics/Number.h>
#include <xrpl/basics/contract.h>
#include <xrpl/beast/utility/Zero.h>
@@ -69,7 +70,13 @@ public:
constexpr XRPAmount
operator*(value_type const& rhs) const
{
return XRPAmount{drops_ * rhs};
if (auto const result = checkedMul(drops_, rhs))
return XRPAmount{*result};
// Not reached in constant evaluation: an overflowing constant
// expression fails to compile instead.
XRPAmount product{*this};
product *= rhs;
return product;
}
friend constexpr XRPAmount
@@ -82,37 +89,23 @@ public:
XRPAmount&
operator+=(XRPAmount const& other)
{
drops_ += other.drops();
return *this;
return *this += other.drops();
}
XRPAmount&
operator-=(XRPAmount const& other)
{
drops_ -= other.drops();
return *this;
return *this -= other.drops();
}
XRPAmount&
operator+=(value_type const& rhs)
{
drops_ += rhs;
return *this;
}
operator+=(value_type const& rhs);
XRPAmount&
operator-=(value_type const& rhs)
{
drops_ -= rhs;
return *this;
}
operator-=(value_type const& rhs);
XRPAmount&
operator*=(value_type const& rhs)
{
drops_ *= rhs;
return *this;
}
operator*=(value_type const& rhs);
XRPAmount
operator-() const
+1 -1
View File
@@ -61,7 +61,6 @@ XRPL_FIX (InnerObjTemplate2, Supported::Yes, VoteBehavior::DefaultNo
XRPL_FIX (EnforceNFTokenTrustline, Supported::Yes, VoteBehavior::DefaultNo)
XRPL_FIX (ReducedOffersV2, Supported::Yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(NFTokenMintOffer, Supported::Yes, VoteBehavior::DefaultNo)
XRPL_FIX (AMMv1_1, Supported::Yes, VoteBehavior::DefaultNo)
XRPL_FIX (PreviousTxnID, Supported::Yes, VoteBehavior::DefaultNo)
XRPL_FIX (XChainRewardRounding, Supported::Yes, VoteBehavior::DefaultNo)
XRPL_FIX (EmptyDID, Supported::Yes, VoteBehavior::DefaultNo)
@@ -107,6 +106,7 @@ XRPL_RETIRE_FIX(1623)
XRPL_RETIRE_FIX(1781)
XRPL_RETIRE_FIX(AmendmentMajorityCalc)
XRPL_RETIRE_FIX(AMMOverflowOffer)
XRPL_RETIRE_FIX(AMMv1_1)
XRPL_RETIRE_FIX(CheckThreading)
XRPL_RETIRE_FIX(DisallowIncomingV1)
XRPL_RETIRE_FIX(InnerObjTemplate)
@@ -417,6 +417,7 @@ LEDGER_ENTRY(ltMPTOKEN_ISSUANCE, 0x007e, MPTokenIssuance, mpt_issuance, ({
{sfAuditorEncryptionKey, SoeOptional},
{sfIssuerKeyEpoch, SoeOptional},
{sfAuditorKeyEpoch, SoeOptional},
{sfInitialIssuerEncryptionKey, SoeOptional},
{sfConfidentialOutstandingAmount, SoeDefault},
}))
@@ -438,6 +439,7 @@ LEDGER_ENTRY(ltMPTOKEN, 0x007f, MPToken, mptoken, ({
{sfAuditorEncryptedBalance, SoeOptional},
{sfIssuerKeyMirrorEpoch, SoeOptional},
{sfAuditorKeyMirrorEpoch, SoeOptional},
{sfIssuerMirrorEncryptionKey, SoeOptional},
{sfHolderEncryptionKey, SoeOptional},
{sfRecoveryKey, SoeOptional},
}))
@@ -333,6 +333,8 @@ TYPED_SFIELD(sfAmountCommitment, VL, 45)
TYPED_SFIELD(sfBalanceCommitment, VL, 46)
TYPED_SFIELD(sfBytecode, VL, 47)
TYPED_SFIELD(sfRecoveryKey, VL, 48)
TYPED_SFIELD(sfInitialIssuerEncryptionKey, VL, 49)
TYPED_SFIELD(sfIssuerMirrorEncryptionKey, VL, 50)
// account (common)
TYPED_SFIELD(sfAccount, ACCOUNT, 1)
@@ -1175,6 +1175,20 @@ TRANSACTION(ttTRANSACTION_PROPOSAL_CREATE, 95, TransactionProposalCreate,
{sfExpiration, SoeRequired},
}))
// ttTRANSACTION_PROPOSAL_SIGN arrives in a later PR and takes the next unassigned type.
/** This transaction deletes a TransactionProposal, releasing its owner's
reserve. While the proposal is live only its Owner or its target account
may cancel; once it is terminal anyone may (On-Chain Cosigner spec §7). */
#if TRANSACTION_INCLUDE
# include <xrpl/tx/transactors/proposal/TransactionProposalCancel.h>
#endif
TRANSACTION(ttTRANSACTION_PROPOSAL_CANCEL, 94, TransactionProposalCancel,
({.amendment = featureCosign}),
({
{sfProposalID, SoeRequired},
}))
/** This system-generated transaction type is used to update the status of the various amendments.
For details, see: https://xrpl.org/amendments.html
+3
View File
@@ -252,6 +252,9 @@ JSS(expected_date); // out: any (warnings)
JSS(expected_date_UTC); // out: any (warnings)
JSS(expected_ledger_size); // out: TxQ
JSS(expiration); // out: AccountOffers, AccountChannels, ValidatorList, amm_info
JSS(gas_limit); // out: NetworkOPs
JSS(bytecode_size_limit); // out: NetworkOPs
JSS(gas_price); // out: NetworkOPs
JSS(fail_hard); // in: Sign, Submit
JSS(failed); // out: InboundLedger
JSS(feature); // in: Feature
@@ -316,6 +316,30 @@ public:
return this->sle_->isFieldPresent(sfAuditorKeyMirrorEpoch);
}
/**
* @brief Get sfIssuerMirrorEncryptionKey (SoeOptional)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_VL::type::value_type>
getIssuerMirrorEncryptionKey() const
{
if (hasIssuerMirrorEncryptionKey())
return this->sle_->at(sfIssuerMirrorEncryptionKey);
return std::nullopt;
}
/**
* @brief Check if sfIssuerMirrorEncryptionKey is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasIssuerMirrorEncryptionKey() const
{
return this->sle_->isFieldPresent(sfIssuerMirrorEncryptionKey);
}
/**
* @brief Get sfHolderEncryptionKey (SoeOptional)
* @return The field value, or std::nullopt if not present.
@@ -565,6 +589,17 @@ public:
return *this;
}
/**
* @brief Set sfIssuerMirrorEncryptionKey (SoeOptional)
* @return Reference to this builder for method chaining.
*/
MPTokenBuilder&
setIssuerMirrorEncryptionKey(std::decay_t<typename SF_VL::type::value_type> const& value)
{
object_[sfIssuerMirrorEncryptionKey] = value;
return *this;
}
/**
* @brief Set sfHolderEncryptionKey (SoeOptional)
* @return Reference to this builder for method chaining.
@@ -399,6 +399,30 @@ public:
return this->sle_->isFieldPresent(sfAuditorKeyEpoch);
}
/**
* @brief Get sfInitialIssuerEncryptionKey (SoeOptional)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_VL::type::value_type>
getInitialIssuerEncryptionKey() const
{
if (hasInitialIssuerEncryptionKey())
return this->sle_->at(sfInitialIssuerEncryptionKey);
return std::nullopt;
}
/**
* @brief Check if sfInitialIssuerEncryptionKey is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasInitialIssuerEncryptionKey() const
{
return this->sle_->isFieldPresent(sfInitialIssuerEncryptionKey);
}
/**
* @brief Get sfConfidentialOutstandingAmount (SoeDefault)
* @return The field value, or std::nullopt if not present.
@@ -670,6 +694,17 @@ public:
return *this;
}
/**
* @brief Set sfInitialIssuerEncryptionKey (SoeOptional)
* @return Reference to this builder for method chaining.
*/
MPTokenIssuanceBuilder&
setInitialIssuerEncryptionKey(std::decay_t<typename SF_VL::type::value_type> const& value)
{
object_[sfInitialIssuerEncryptionKey] = value;
return *this;
}
/**
* @brief Set sfConfidentialOutstandingAmount (SoeDefault)
* @return Reference to this builder for method chaining.
@@ -0,0 +1,131 @@
// This file is auto-generated. Do not edit.
#pragma once
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/STParsedJSON.h>
#include <xrpl/protocol/jss.h>
#include <xrpl/protocol_autogen/TransactionBase.h>
#include <xrpl/protocol_autogen/TransactionBuilderBase.h>
#include <xrpl/json/json_value.h>
#include <stdexcept>
#include <optional>
namespace xrpl::transactions {
class TransactionProposalCancelBuilder;
/**
* @brief Transaction: TransactionProposalCancel
*
* Type: ttTRANSACTION_PROPOSAL_CANCEL (94)
* Delegable: Delegation::NotDelegable
* Amendment: featureCosign
* Privileges: Privilege::NoPriv
*
* Immutable wrapper around STTx providing type-safe field access.
* Use TransactionProposalCancelBuilder to construct new transactions.
*/
class TransactionProposalCancel : public TransactionBase
{
public:
static constexpr xrpl::TxType txType = ttTRANSACTION_PROPOSAL_CANCEL;
/**
* @brief Construct a TransactionProposalCancel transaction wrapper from an existing STTx object.
* @throws std::runtime_error if the transaction type doesn't match.
*/
explicit TransactionProposalCancel(std::shared_ptr<STTx const> tx)
: TransactionBase(std::move(tx))
{
// Verify transaction type
if (tx_->getTxnType() != txType)
{
throw std::runtime_error("Invalid transaction type for TransactionProposalCancel");
}
}
// Transaction-specific field getters
/**
* @brief Get sfProposalID (SoeRequired)
* @return The field value.
*/
[[nodiscard]]
SF_UINT256::type::value_type
getProposalID() const
{
return this->tx_->at(sfProposalID);
}
};
/**
* @brief Builder for TransactionProposalCancel transactions.
*
* Provides a fluent interface for constructing transactions with method chaining.
* Uses STObject internally for flexible transaction construction.
* Inherits common field setters from TransactionBuilderBase.
*/
class TransactionProposalCancelBuilder : public TransactionBuilderBase<TransactionProposalCancelBuilder>
{
public:
/**
* @brief Construct a new TransactionProposalCancelBuilder with required fields.
* @param account The account initiating the transaction.
* @param proposalID The sfProposalID field value.
* @param sequence Optional sequence number for the transaction.
* @param fee Optional fee for the transaction.
*/
TransactionProposalCancelBuilder(SF_ACCOUNT::type::value_type account,
std::decay_t<typename SF_UINT256::type::value_type> const& proposalID, std::optional<SF_UINT32::type::value_type> sequence = std::nullopt,
std::optional<SF_AMOUNT::type::value_type> fee = std::nullopt
)
: TransactionBuilderBase<TransactionProposalCancelBuilder>(ttTRANSACTION_PROPOSAL_CANCEL, account, sequence, fee)
{
setProposalID(proposalID);
}
/**
* @brief Construct a TransactionProposalCancelBuilder from an existing STTx object.
* @param tx The existing transaction to copy from.
* @throws std::runtime_error if the transaction type doesn't match.
*/
TransactionProposalCancelBuilder(std::shared_ptr<STTx const> tx)
{
if (tx->getTxnType() != ttTRANSACTION_PROPOSAL_CANCEL)
{
throw std::runtime_error("Invalid transaction type for TransactionProposalCancelBuilder");
}
object_ = *tx;
}
/**
* @brief Transaction-specific field setters
*/
/**
* @brief Set sfProposalID (SoeRequired)
* @return Reference to this builder for method chaining.
*/
TransactionProposalCancelBuilder&
setProposalID(std::decay_t<typename SF_UINT256::type::value_type> const& value)
{
object_[sfProposalID] = value;
return *this;
}
/**
* @brief Build and return the TransactionProposalCancel wrapper.
* @param publicKey The public key for signing.
* @param secretKey The secret key for signing.
* @return The constructed transaction wrapper.
*/
TransactionProposalCancel
build(PublicKey const& publicKey, SecretKey const& secretKey)
{
sign(publicKey, secretKey);
return TransactionProposalCancel{std::make_shared<STTx>(std::move(object_))};
}
};
} // namespace xrpl::transactions
+121 -9
View File
@@ -3,9 +3,12 @@
#include <xrpl/basics/KeyCache.h>
#include <xrpl/basics/TaggedCache.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/partitioned_unordered_map.h>
#include <xrpl/beast/hash/hash_append.h>
#include <xrpl/beast/insight/Collector.h>
#include <xrpl/beast/insight/NullCollector.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/shamap/SHAMapNodeID.h>
#include <atomic>
#include <chrono>
@@ -15,6 +18,112 @@
namespace xrpl {
/**
* Names a subtree whose descendants are all resident, by hash and by position.
*
* A node's hash covers its children but neither its depth nor its path, and one
* cache answers for every map built from a NodeFamily, so the position is what
* makes a hit answer for the position asked about and no other. The depth is
* kept beside the id because SHAMapNodeID masks its id to its own depth, so an
* all-zero id belongs to the root and to every all-zero prefix alike.
*
* Held as two fields rather than as a SHAMapNodeID, which is a CountedObject:
* one per entry would report the cache's population as live node IDs in the
* get_counts RPC, and would put an atomic increment on every temporary key a
* lookup builds.
*/
struct FullBelowKey
{
/**
* The hash of the subtree.
*/
UInt256 hash;
/**
* The masked id of the node the subtree was completed at.
*/
UInt256 position;
/**
* The depth of that node.
*/
std::uint32_t depth{0};
/**
* @param subtreeHash The hash of the subtree whose descendants are all
* resident.
* @param nodeID The node the subtree was completed at.
*/
FullBelowKey(UInt256 const& subtreeHash, SHAMapNodeID const& nodeID)
: hash(subtreeHash)
, position(nodeID.getNodeID())
, depth(static_cast<std::uint32_t>(nodeID.getDepth()))
{
}
/**
* The same key, named by the node above the position and the branch taken.
*
* Produces the fields the constructor above would for that child's own node
* ID, so the two forms name one entry.
*
* @param subtreeHash The hash of the subtree that is fully resident.
* @param parentID The node above the one the subtree was completed at.
* @param branch The branch of that node leading to it.
*/
FullBelowKey(UInt256 const& subtreeHash, SHAMapNodeID const& parentID, unsigned int branch)
: hash(subtreeHash)
, position(childNodeID(parentID.getNodeID(), parentID.getDepth(), branch))
, depth(static_cast<std::uint32_t>(parentID.getDepth() + 1))
{
}
[[nodiscard]] bool
operator==(FullBelowKey const& other) const = default;
};
// The per-entry cost of this cache is computed from these sizes, so pin the layout that arithmetic
// assumes: the three fields sit back to back with no padding.
static_assert(
sizeof(FullBelowKey) == (2 * sizeof(UInt256)) + sizeof(std::uint32_t),
"FullBelowKey must stay free of padding");
static_assert(
alignof(FullBelowKey) == alignof(std::uint32_t),
"FullBelowKey's lack of padding rests on its alignment");
/**
* Feed a key to a hasher, field by field.
*
* Written out rather than hashed in one call over the object's bytes, since
* beast::IsUniquelyRepresented is opt-in per type and has no case for a
* user-defined struct, whatever its layout.
*
* @param h The hasher to feed.
* @param key The key to hash.
*/
template <class Hasher>
void
hash_append(Hasher& h, FullBelowKey const& key) noexcept
{
using beast::hash_append;
hash_append(h, key.hash, key.position, key.depth);
}
/**
* Choose which partition of the cache's map a key lives in.
*
* Taken from the subtree hash, which is already uniformly distributed.
*
* @param key The key to place.
* @return The value PartitionedUnorderedMap reduces by its partition count.
*/
template <>
inline std::size_t
extract(FullBelowKey const& key)
{
return extract(key.hash);
}
namespace detail {
/**
@@ -24,12 +133,12 @@ namespace detail {
class BasicFullBelowCache
{
private:
using CacheType = KeyCache;
using CacheType = KeyCache<FullBelowKey>;
public:
static constexpr auto kDefaultCacheTargetSize = 0;
using key_type = UInt256;
using key_type = FullBelowKey;
using ClockType = CacheType::ClockType;
/**
@@ -86,13 +195,15 @@ public:
* Refresh the last access time of an item, if it exists.
* Thread safety:
* Safe to call from any thread.
* @param key The key to refresh.
* @return `true` If the key exists.
* @param hash The hash of the subtree to ask about.
* @param parentID The node above the position to ask about it at.
* @param branch The branch of that node leading to that position.
* @return `true` If that subtree is recorded as resident at that position.
*/
bool
touchIfExists(key_type const& key)
touchIfExists(UInt256 const& hash, SHAMapNodeID const& parentID, unsigned int branch)
{
return cache_.touchIfExists(key);
return cache_.touchIfExists(key_type{hash, parentID, branch});
}
/**
@@ -101,12 +212,13 @@ public:
* be refreshed.
* Thread safety:
* Safe to call from any thread.
* @param key The key to insert.
* @param hash The hash of the subtree whose descendants are all resident.
* @param nodeID The node that subtree was completed at.
*/
void
insert(key_type const& key)
insert(UInt256 const& hash, SHAMapNodeID const& nodeID)
{
cache_.insert(key);
cache_.insert(key_type{hash, nodeID});
}
/**
+8 -5
View File
@@ -278,11 +278,14 @@ This cache remembers which trie keys have all of their children resident in a
when creating the missing nodes list. Missing nodes are those nodes that a
`SHAMap` refers to but that are not stored in the local database.
As a depth-first walk of a `SHAMap` is performed, if an inner node answers true to
`isFullBelow()` then it is known that none of this node's children are missing
nodes, and thus that subtree does not need to be walked. These nodes are stored
in the FullBelowCache. Subsequent walks check the FullBelowCache first when
encountering a node, and ignore that subtree if found.
As a depth-first walk of a `SHAMap` completes an inner node, meaning none of the
nodes below it are missing, the node's hash and position are stored in the
FullBelowCache. Subsequent walks check the FullBelowCache when encountering a
child, and skip that subtree if found. The entry is keyed by position as well as
hash, since a hash commits to a node's contents rather than its place, and the
same node object is shared by hash between maps and positions. For the same
reason the inner node's own `isFullBelow()` flag is written and read for a
walk's root alone, where the position is fixed.
## `SHAMapTreeNode`
+617 -116
View File
@@ -2,6 +2,7 @@
#include <xrpl/basics/Blob.h>
#include <xrpl/basics/IntrusivePointer.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/SHAMapHash.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/utility/Journal.h>
@@ -17,6 +18,7 @@
#include <xrpl/shamap/SHAMapNodeID.h>
#include <xrpl/shamap/SHAMapTreeNode.h>
#include <atomic>
#include <condition_variable>
#include <cstddef>
#include <cstdint>
@@ -30,6 +32,7 @@
#include <set>
#include <stack>
#include <tuple>
#include <type_traits>
#include <utility>
#include <vector>
@@ -40,7 +43,7 @@ class SHAMapSyncFilter;
/**
* Describes the current state of a given SHAMap
*/
enum class SHAMapState {
enum class SHAMapState : std::uint8_t {
/**
* The map is in flux and objects can be added and removed.
*
@@ -71,20 +74,21 @@ enum class SHAMapState {
};
/**
* A SHAMap is both a radix tree with a fan-out of 16 and a Merkle tree.
* A SHAMap is both a trie with a fan-out of 16 and a Merkle tree.
*
* A radix tree is a tree with two properties:
* A trie holds a key in the position of its nodes: the path from the root down
* to a node spells out the prefix every key below it shares (the "prefix
* property"). A SHAMap spends one nibble of the 256-bit key per level, so an
* inner node has at most 16 children and a leaf sits at depth 64 at the
* deepest. A leaf also carries its own full key, so a reader can check that it
* was reached through the branches that key names.
*
* 1. The key for a node is represented by the node's position in the tree
* (the "prefix property").
* 2. A node with only one child is merged with that child
* (the "merge property")
* An insert creates an inner node at every nibble two keys share, and a delete
* collapses a chain that reduces to one leaf. Every edge spans exactly one
* nibble, so a path has one entry per level and 65 entries at the most, and a
* path's length names each node's depth. Traversal relies on that.
*
* These properties result in a significantly smaller memory footprint for
* a radix tree.
*
* A fan-out of 16 means that each node in the tree has at most 16
* children. See https://en.wikipedia.org/wiki/Radix_tree
* See https://en.wikipedia.org/wiki/Trie
*
* A Merkle tree is a tree where each non-leaf node is labelled with the hash
* of the combined labels of its children nodes.
@@ -120,21 +124,43 @@ private:
*/
std::uint32_t cowid_ = 1;
// ledgerSeq_, state_ and full_ are touched on the nodestore fetch path and on a
// getMissingNodes() walk, neither of which may block, so pin them lock-free.
static_assert(std::atomic<std::uint32_t>::is_always_lock_free);
static_assert(std::atomic<SHAMapState>::is_always_lock_free);
static_assert(std::atomic<bool>::is_always_lock_free);
/**
* The sequence of the ledger that this map references, if any.
*
* Written by Ledger::setFull() and by InboundLedger, and read on a
* nodestore reader thread. Relaxed both ways, since it is only a lookup
* hint for a store keyed by hash.
*/
std::uint32_t ledgerSeq_ = 0;
std::atomic<std::uint32_t> ledgerSeq_ = 0;
SHAMapTreeNodePtr root_;
mutable SHAMapState state_;
/**
* The map's state. Atomic because a getMissingNodes() walk writes it with
* the acquisition's lock released. Mutable because a const descend() can
* record the Invalid verdict.
*/
mutable std::atomic<SHAMapState> state_;
SHAMapType const type_;
bool backed_ = true; // Map is backed by the database
mutable bool full_ = false; // Map is believed complete in database
bool backed_ = true; // Map is backed by the database
/**
* Map is believed complete in database.
*
* Atomic because finishFetch() clears it on any nodestore reader thread,
* several of which can run at once.
*/
mutable std::atomic<bool> full_ = false;
public:
/**
* Number of children each non-leaf node has (the 'radix tree' part of the
* map)
* Number of children each non-leaf node has, which is the trie's fan-out
*/
static constexpr unsigned int kBranchFactor = SHAMapInnerNode::kBranchFactor;
@@ -143,6 +169,18 @@ public:
*/
static constexpr unsigned int kLeafDepth = 64;
/**
* Whether only a leaf may occupy a position at `depth`.
*
* @param depth the depth to judge.
* @return whether that depth is at or past kLeafDepth.
*/
[[nodiscard]] static constexpr bool
isLeafDepth(unsigned int depth)
{
return depth >= kLeafDepth;
}
using DeltaItem =
std::pair<boost::intrusive_ptr<SHAMapItem const>, boost::intrusive_ptr<SHAMapItem const>>;
using Delta = std::map<UInt256, DeltaItem>;
@@ -152,7 +190,14 @@ public:
SHAMap&
operator=(SHAMap const&) = delete;
// Take a snapshot of the given map:
/**
* Take a snapshot of the given map.
*
* @param other The map to snapshot. An Invalid source yields an Invalid
* snapshot.
* @param isMutable Whether the snapshot may be modified. Ignored when other
* is Invalid.
*/
SHAMap(SHAMap const& other, bool isMutable);
// build new map
@@ -190,17 +235,33 @@ public:
//--------------------------------------------------------------------------
// Returns a new map that's a snapshot of this one.
// Handles copy on write for mutable snapshots.
/**
* Return a new map that is a snapshot of this one.
*
* Handles copy on write for mutable snapshots. An invalid map yields an
* invalid snapshot.
*
* @param isMutable Whether the snapshot may be modified.
* @return The snapshot.
*/
std::shared_ptr<SHAMap>
snapShot(bool isMutable) const;
/* Mark this SHAMap as "should be full", indicating
that the local server wants all the corresponding nodes
in durable storage.
*/
/**
* Mark this map as "should be full": the local server wants every node
* it references in durable storage, so a nodestore miss is a gap to report.
*
* Publishes the ledger sequence before the flag. The sequence store is
* relaxed and the flag store is release, and finishFetch() reads the
* sequence only after acquiring the flag, so the one miss that withdraws
* the flag always reports the sequence stored here. Keeping both stores in
* one place is what makes that order impossible to get wrong.
*
* @param ledgerSeq Sequence of the ledger this map belongs to, which the
* gap report names.
*/
void
setFull();
setFull(std::uint32_t ledgerSeq);
void
setLedgerSeq(std::uint32_t lseq);
@@ -243,9 +304,11 @@ public:
/**
* Find the first item after the given item.
*
* @param id the identifier of the item.
*
* @note The item does not need to exist.
* @param id the identifier of the item, which need not exist in the map.
* @return an iterator at the first item with a greater key, or end() if the
* map holds no greater key.
* @throws SHAMapMissingNode if the map cannot be walked. end() is the
* separate answer that the map holds no greater key.
*/
ConstIterator
upperBound(UInt256 const& id) const;
@@ -253,9 +316,11 @@ public:
/**
* Find the object with the greatest object id smaller than the input id.
*
* @param id the identifier of the item.
*
* @note The item does not need to exist.
* @param id the identifier of the item, which need not exist in the map.
* @return an iterator at the last item with a smaller key, or end() if the
* map holds no smaller key.
* @throws SHAMapMissingNode if the map cannot be walked. end() is the
* separate answer that the map holds no smaller key.
*/
ConstIterator
lowerBound(UInt256 const& id) const;
@@ -296,9 +361,13 @@ public:
* concurrency, to discover nodes referenced in the
* SHAMap but not available locally.
*
* Only a leaf may occupy a position at or beyond kLeafDepth. A map that
* breaks that is marked Invalid and the traversal is abandoned, so callers
* ask isValid() to tell an empty result from a satisfied map.
*
* @param maxNodes The maximum number of found nodes to return
* @param filter The filter to use when retrieving nodes
* @param return The nodes known to be missing
* @return The nodes known to be missing, or empty if the map is Invalid
*/
std::vector<std::pair<SHAMapNodeID, UInt256>>
getMissingNodes(int maxNodes, SHAMapSyncFilter const* filter);
@@ -341,6 +410,9 @@ public:
* This function is used when receiving the root node of a SHAMap from a peer during ledger
* synchronization. The node must already have been deserialized.
*
* A root offered under a hash the map does not hold names another tree and
* is reported as invalid data. A root matching that hash is a duplicate.
*
* @param hash The expected hash of the root node.
* @param rootNode A deserialized root node to add.
* @param filter Optional sync filter to track received nodes.
@@ -360,14 +432,26 @@ public:
* is inserted at the position specified by nodeID. The node must already have been
* deserialized.
*
* A node that no valid tree can hold makes the map Invalid, which is
* terminal. An acquisition reaching that verdict gives up.
*
* @param nodeID The position in the tree where this node belongs.
* @param treeNode A deserialized tree node to add.
* @param filter Optional sync filter to track received nodes.
* @return Status indicating whether the node was useful, duplicate, or invalid.
*
* @note This function expects the treeNode to be a valid, deserialized SHAMapTreeNode. The
* caller is responsible for deserialization and basic validation before calling this
* function. This also means that the nodeID must be consistent with the node's content.
* @note The caller is responsible for deserialization. The position is
* checked here, in three steps that share two verdicts. A node
* the descent reaches at a position other than the one nodeID
* claims is refused with invalid(), and the map stays valid, since
* only the label was wrong. A leaf that hash-verified at the
* position nodeID claims, but whose own key does not lie under
* nodeID, condemns the map and returns mapInvalidated(). A node the
* descent itself condemns on the way, resolved from the local store
* or the filter rather than supplied by the caller, leaves the map
* Invalid and returns invalid(), since that verdict is not the
* caller's doing. Any map outside Synching, including one already
* condemned, returns duplicate().
*/
SHAMapAddNode
addKnownNode(
@@ -375,16 +459,57 @@ public:
SHAMapTreeNodePtr treeNode,
SHAMapSyncFilter const* filter);
// status functions
void
/**
* Mark this map as immutable, so it can no longer be modified.
*
* @return false if the map is Invalid and was left unchanged, true
* otherwise.
*/
[[nodiscard]] bool
setImmutable();
bool
/**
* @return Whether the map is being synced against a hash it was given, so
* its hash is fixed while nodes may still be added.
*/
[[nodiscard]] bool
isSynching() const;
/**
* @return Whether the map is settled, with its hash and nodes fixed.
*/
[[nodiscard]] bool
isImmutable() const;
/**
* Mark this map as syncing, fixing its hash while still allowing missing
* nodes to be added.
*
* The map must be freshly constructed. The body asserts that the map is not
* Invalid, so a caller that ignores the precondition stops a build with
* assertions enabled.
*/
void
setSynching();
/**
* Mark this map as no longer syncing, so it can be modified again.
*
* Moves only a Synching map. An Immutable map stays settled, a Modifying
* map has nothing to clear, and Invalid is terminal.
*/
void
clearSynching();
bool
/**
* Whether the map can still be the map it claims to be.
*
* A map that is merely missing nodes is valid, and stays valid until
* something proves the tree it is syncing against cannot exist.
*
* @return Whether the map has not been proven impossible.
*/
[[nodiscard]] bool
isValid() const;
// caution: otherMap must be accessed only by this function
@@ -421,109 +546,362 @@ public:
private:
/**
* A path from the root of the map down to some node, pairing each node with the ID naming its
* position.
* Whether placing `node` one level below `parentDepth` leaves it no room.
*
* The two halves of an entry must agree, and the only way to get that wrong is to compute an ID
* from the wrong branch. So this type does not accept an ID at all: every push takes the branch
* being descended and derives the ID itself, so a node and its ID cannot disagree. Reads are
* exposed through the same accessors a std::stack would offer.
* Only a leaf may sit at kLeafDepth, and only a parent above kLeafDepth has
* a nibble left to record the branch taken. Both bounds are needed, since a
* leaf one level above kLeafDepth is allowed. Position is judged separately,
* by the walk that keeps a path.
*
* The conjunct order is load bearing: the depth is tested first, so the
* virtual isInner() call runs only at the one level where the bound
* applies.
*
* @param parentDepth the depth of the node being descended from.
* @param node the node about to be placed one level below it.
* @return whether that placement is past the deepest level this kind of
* node may occupy.
*/
[[nodiscard]] static bool
pastLeafDepth(unsigned int parentDepth, SHAMapTreeNode const& node)
{
return isLeafDepth(parentDepth + 1u) && (node.isInner() || isLeafDepth(parentDepth));
}
/**
* A root-down path of nodes through the map.
*
* Entry `i` is the node at depth `i`, since every edge spans one nibble
* (see the class docstring). Only the node is stored. A caller that needs a
* position reads a depth and takes the nibbles from its own key.
*/
class NodePathStack
{
public:
/**
* @return whether the path holds no node at all.
*/
[[nodiscard]] bool
empty() const
{
return stack_.empty();
return path_.empty();
}
/**
* @return how many nodes the path holds, one more than the last node's
* depth.
*/
[[nodiscard]] std::size_t
size() const
{
return stack_.size();
return path_.size();
}
[[nodiscard]] std::pair<SHAMapTreeNodePtr, SHAMapNodeID> const&
/**
* The node at the end of the path.
*
* @return a reference into the path, which a later push may invalidate
* by reallocating, so a caller that pushes must ask again. An
* empty path yields a null node the caller can test.
*/
[[nodiscard]] SHAMapTreeNodePtr const&
top() const
{
XRPL_ASSERT(!stack_.empty(), "xrpl::SHAMap::NodePathStack::top : non-empty stack");
return stack_.top();
if (path_.empty())
{
// LCOV_EXCL_START
UNREACHABLE("xrpl::SHAMap::NodePathStack::top : empty stack");
static SHAMapTreeNodePtr const kEmpty;
return kEmpty;
// LCOV_EXCL_STOP
}
return path_.back();
}
/**
* The depth of the node at the end of the path.
*
* @return the depth, which is the entry's own index. Zero on an empty
* path.
*/
[[nodiscard]] unsigned int
topDepth() const
{
if (path_.empty())
{
// LCOV_EXCL_START
UNREACHABLE("xrpl::SHAMap::NodePathStack::topDepth : empty stack");
return 0;
// LCOV_EXCL_STOP
}
return static_cast<unsigned int>(path_.size() - 1);
}
/**
* Shorten the path by one node. An empty path is left as it is.
*/
void
pop()
{
XRPL_ASSERT(!stack_.empty(), "xrpl::SHAMap::NodePathStack::pop : non-empty stack");
stack_.pop();
if (path_.empty())
{
// LCOV_EXCL_START
UNREACHABLE("xrpl::SHAMap::NodePathStack::pop : empty stack");
return;
// LCOV_EXCL_STOP
}
path_.pop_back();
}
/**
* Discard the whole path.
*/
void
clear()
{
stack_ = {};
path_.clear();
pathKey_ = UInt256{};
}
/**
* Start a path at the root of the map, whose ID is the zero-depth ID by definition.
*/
void
pushRoot(SHAMapTreeNodePtr node)
{
XRPL_ASSERT(stack_.empty(), "xrpl::SHAMap::NodePathStack::pushRoot : empty stack");
stack_.emplace(std::move(node), SHAMapNodeID{});
}
/**
* Extend the path to the child of the current node reached by `branch`.
* Shorten the path by one node and hand that node to the caller.
*
* A node keeps the depth it was reached at, never a normalized kLeafDepth. Only a leaf may
* sit at kLeafDepth, since an inner node there would have no branch left to select.
*/
void
pushChild(SHAMapTreeNodePtr node, unsigned int branch)
{
XRPL_ASSERT(node, "xrpl::SHAMap::NodePathStack::pushChild : non-null node input");
XRPL_ASSERT(
!stack_.empty(), "xrpl::SHAMap::NodePathStack::pushChild : non-empty stack");
auto const childID = stack_.top().second.getChildNodeID(branch);
XRPL_ASSERT_IF(
node->isInner(),
childID.getDepth() < kLeafDepth,
"xrpl::SHAMap::NodePathStack::pushChild : inner node above leaf depth");
XRPL_ASSERT_IF(
node->isLeaf(),
childID.isPrefixOf(leafKey(*node)),
"xrpl::SHAMap::NodePathStack::pushChild : leaf key below branch");
stack_.emplace(std::move(node), childID);
}
/**
* Extend the path to a node lying on the path to `target`.
* Moves the node out, transferring the existing reference.
*
* For nodes not reached by descending a known branch: the walk tracks only the key it is
* heading for, or the node is newly created. Either way `target` selects the branch.
* @return the node that was at the end of the path, or an empty
* pointer if there was none.
*/
void
pushNode(SHAMapTreeNodePtr node, UInt256 const& target)
[[nodiscard]] SHAMapTreeNodePtr
releaseNode()
{
if (stack_.empty())
if (path_.empty())
{
pushRoot(std::move(node));
// LCOV_EXCL_START
UNREACHABLE("xrpl::SHAMap::NodePathStack::releaseNode : empty stack");
return {};
// LCOV_EXCL_STOP
}
auto node = std::move(path_.back());
path_.pop_back();
return node;
}
/**
* Shorten the path by one node and hand it over as a `Node`.
*
* @tparam Node SHAMapInnerNode or SHAMapLeafNode.
* @return the node that was at the end of the path, or an empty pointer
* if the path was empty or that node is not a `Node`. The path
* is shortened either way.
*/
template <class Node>
[[nodiscard]] intr_ptr::SharedPtr<Node>
releaseNodeAs()
{
static_assert(
std::is_same_v<Node, SHAMapInnerNode> || std::is_same_v<Node, SHAMapLeafNode>,
"releaseNodeAs serves the two concrete node kinds");
auto node = releaseNode();
if (!node)
{
// The other half of releaseNode()'s empty-path contract, asserted there by the
// UNREACHABLE in its own LCOV_EXCL block.
return {}; // LCOV_EXCL_LINE
}
if constexpr (std::is_same_v<Node, SHAMapInnerNode>)
{
// A leaf sits only at the end of a path, which both callers asking for an inner
// node have released first. The other half of the contract their own UNREACHABLEs
// assert, in SHAMap::dirtyUp and SHAMap::delItem.
if (!node->isInner())
return {}; // LCOV_EXCL_LINE
}
else
{
pushChild(std::move(node), selectBranch(stack_.top().second, target));
if (!node->isLeaf())
return {};
}
return intr_ptr::staticPointerCast<Node>(std::move(node));
}
/**
* Start a path at the root of the map, which sits at depth zero by
* definition.
*
* @return false, leaving the path unchanged, if a path was already
* started.
*/
[[nodiscard]] bool
pushRoot(SHAMapTreeNodePtr node)
{
if (!path_.empty())
{
// LCOV_EXCL_START
UNREACHABLE("xrpl::SHAMap::NodePathStack::pushRoot : non-empty stack");
return false;
// LCOV_EXCL_STOP
}
// Reserved here rather than in the constructor, to keep SHAMap::end()
// allocation-free. A path holds at most kLeafDepth + 1 entries, so no push on a path
// started here reallocates. A copy is not given that room, so top()'s invalidation
// rule still holds.
path_.reserve(kLeafDepth + 1u);
path_.push_back(std::move(node));
return true;
}
/**
* Extend the path to the child of the node at its end reached by
* `branch`.
*
* Only a leaf may sit at kLeafDepth, since an inner node there would
* have no branch left to select.
*
* @param node the child to append.
* @param branch the branch of the current node that `node` was reached
* through, which serves to judge the node offered.
* @return false if there is no node to descend from or to push, no
* branch of that number, no room left below for the kind of
* node offered, or a leaf whose own key does not select
* `branch`. The path keeps its nodes.
*/
[[nodiscard]] bool
pushChild(SHAMapTreeNodePtr node, unsigned int branch)
{
if (path_.empty() || !node || branch >= kBranchFactor)
{
// LCOV_EXCL_START
UNREACHABLE("xrpl::SHAMap::NodePathStack::pushChild : no child to push");
return false;
// LCOV_EXCL_STOP
}
// A node must have room below it at the depth it is offered for. Reachable, since a
// node resolved by hash carries no position of its own.
auto const parentDepth = topDepth();
bool const tooDeep = pastLeafDepth(parentDepth, *node);
SOMETIMES(tooDeep, "xrpl::SHAMap::NodePathStack::pushChild : child past leaf depth");
if (tooDeep)
{
return false;
}
// A leaf's key must agree with every branch recorded above it, which the chain in
// `pathKey_` supplies. A path's length gives only the depths.
pathKey_ = childNodeID(pathKey_, parentDepth, branch);
// The test takes the depth and the two keys, so no SHAMapNodeID is built: it
// is a CountedObject, and one per push would report as a live node ID.
bool const misplaced =
node->isLeaf() && !samePositionAtDepth(parentDepth + 1u, pathKey_, leafKey(*node));
SOMETIMES(
misplaced, "xrpl::SHAMap::NodePathStack::pushChild : leaf key outside branch");
if (misplaced)
{
return false;
}
path_.push_back(std::move(node));
return true;
}
/**
* Extend the path by one node lying on the way to `target`, starting
* it if it is empty.
*
* For nodes not reached by descending a known branch: the walk tracks
* only the key it is heading for, or the node is newly created. Either
* way `target` names the branch.
*
* @param node the node to append.
* @param target the key the walk is heading for.
* @return whatever pushRoot or pushChild returned.
*/
[[nodiscard]] bool
pushNode(SHAMapTreeNodePtr node, UInt256 const& target)
{
if (path_.empty())
{
return pushRoot(std::move(node));
}
return pushChild(std::move(node), selectBranch(topDepth(), target));
}
private:
std::stack<std::pair<SHAMapTreeNodePtr, SHAMapNodeID>> stack_;
// path_[i] holds the node at depth i, by construction: pushRoot starts at depth 0 and
// pushChild only ever appends one level.
std::vector<SHAMapTreeNodePtr> path_;
// The branches descended, one nibble per level: nibble i is the branch taken from depth i.
// Nibbles below the path's end are stale after a pop, so every read masks at the path's
// own depth. childNodeID clears a nibble before writing it, so re-descending overwrites.
UInt256 pathKey_;
};
using DeltaRef =
std::pair<boost::intrusive_ptr<SHAMapItem const>, boost::intrusive_ptr<SHAMapItem const>>;
/**
* The sequence of the ledger this map references, read atomically.
*
* @return The sequence, or zero if the map references no ledger.
*/
[[nodiscard]] std::uint32_t
ledgerSeq() const;
/**
* The current state, read atomically.
*
* Orders state_ alone. The tree's nodes carry no ordering guarantees.
*
* @return The state as of the call, which a concurrent walk may already
* have moved past.
*/
[[nodiscard]] SHAMapState
state() const;
/**
* Record that the map is provably not the one it claims to be.
*
* Cannot fail, since Invalid outranks every other state. Const because a
* const descend() can reach this verdict while resolving a node.
*/
void
setInvalid() const;
/**
* Condemn the map, naming in the log the node whose position proves it.
*
* Const, because a read path can reach this verdict. See setInvalid().
*
* @param node the node that cannot sit where it was offered.
* @param hash the hash it was resolved under.
* @param position the place in the tree it was offered for.
*/
void
condemn(SHAMapTreeNode const& node, SHAMapHash const& hash, SHAMapNodeID const& position) const;
/**
* Move the map to a new state, atomically.
*
* With clearSynching(), the only writer of state_ past construction.
* Invalid is always stored, and every other transition is refused once
* the map is Invalid, which is what makes that verdict terminal.
* clearSynching() is narrower and moves only Synching.
*
* Const to let setInvalid() be const. state_ is mutable, which makes that
* legal.
*
* @param desired The state to move to.
* @return false if the map is Invalid and the requested state is not,
* leaving it unchanged. True otherwise.
*/
bool
trySetState(SHAMapState desired) const;
// tree node cache operations
SHAMapTreeNodePtr
cacheLookup(SHAMapHash const& hash) const;
@@ -550,9 +928,13 @@ private:
dirtyUp(NodePathStack& stack, UInt256 const& target, SHAMapTreeNodePtr terminal);
/**
* Walk towards the specified id, returning the node. Caller must check
* if the return is nullptr, and if not, if the node->peekItem()->key() ==
* id
* Walk towards the specified id, returning the node.
*
* @param id the key to walk towards, which need not be in the map.
* @param stack records the path walked, or nullptr to skip recording it.
* @return the leaf the walk ended on, or nullptr if it ended on an inner
* node or was refused. A returned leaf need not hold `id`, so
* callers compare its key themselves.
*/
SHAMapLeafNode*
walkTowardsKey(UInt256 const& id, NodePathStack* stack = nullptr) const;
@@ -564,10 +946,16 @@ private:
/**
* Unshare the node, allowing it to be modified
*
* @param node the node to unshare.
* @param depth the depth the node sits at, which says whether it is the
* root. A clone of the root has to be adopted as the new root. A
* clone of any other node is hooked up by the caller.
* @return the node, cloned if it was shared.
*/
template <class Node>
intr_ptr::SharedPtr<Node>
unshareNode(intr_ptr::SharedPtr<Node>, SHAMapNodeID const& nodeID);
[[nodiscard]] intr_ptr::SharedPtr<Node>
unshareNode(intr_ptr::SharedPtr<Node> node, unsigned int depth);
/**
* prepare a node to be modified before flushing
@@ -588,8 +976,13 @@ private:
/**
* Returns the first or last item at or below the node already on top of `stack`, extending
* `stack` with the path walked to reach it.
*
* @param stack the path to extend, whose last node the search starts from.
* @param direction whether to take the lowest or the highest branch at
* each level.
* @return the leaf found, or nullptr if no leaf lies below that node.
*/
SHAMapLeafNode*
[[nodiscard]] SHAMapLeafNode*
belowHelper(NodePathStack& stack, BelowDirection direction) const;
/**
@@ -603,7 +996,7 @@ private:
* @param direction First to search upwards from `id`, Last to search downwards.
* @return An iterator to the item found, or end() if no item lies on that side of `id`.
*/
ConstIterator
[[nodiscard]] ConstIterator
boundHelper(UInt256 const& id, BelowDirection direction) const;
// Simple descent
@@ -628,6 +1021,22 @@ private:
bool& pending,
DescendCallback&&) const;
/**
* Resolve the child of `parent` on `branch`, judging where it lands.
*
* Const, yet it records the Invalid verdict through setInvalid(). A new
* caller must therefore be a path that may reach that verdict. Today
* addKnownNode() is the only one.
*
* @param parent the inner node to descend from.
* @param parentID the position of `parent`.
* @param branch the branch of `parent` to resolve, which the caller has
* already bounded.
* @param filter an alternate source of nodes, or null for the store alone.
* @return the child and the child's position. A null child where the branch
* could not be resolved, or where what came back does not belong at
* that position, in which case the map is left Invalid.
*/
std::pair<SHAMapTreeNode*, SHAMapNodeID>
descend(
SHAMapInnerNode* parent,
@@ -728,10 +1137,29 @@ private:
};
// getMissingNodes helper functions
/**
* Examine the remaining branches of one inner node, recording or
* requesting what is missing.
*
* @param mn the walk's shared state, which collects the missing nodes.
* @param node the walk's current position, updated to the node to process
* next.
*/
void
gmnProcessNodes(MissingNodes&, MissingNodes::StackEntry& node);
static void
gmnProcessDeferredReads(MissingNodes&);
gmnProcessNodes(MissingNodes& mn, MissingNodes::StackEntry& node);
/**
* Wait for every read this pass posted, then hook up or record what each
* one resolved.
*
* Drains all of them even after judging the map, since an outstanding read
* holds a pointer to `mn`.
*
* @param mn the walk's shared state, holding the posted reads.
*/
void
gmnProcessDeferredReads(MissingNodes& mn);
// fetch from DB helper function
SHAMapTreeNodePtr
@@ -739,46 +1167,119 @@ private:
};
inline void
SHAMap::setFull()
SHAMap::setFull(std::uint32_t ledgerSeq)
{
full_ = true;
ledgerSeq_.store(ledgerSeq, std::memory_order_relaxed);
full_.store(true, std::memory_order_release);
}
inline void
SHAMap::setLedgerSeq(std::uint32_t lseq)
{
ledgerSeq_ = lseq;
ledgerSeq_.store(lseq, std::memory_order_relaxed);
}
inline void
inline std::uint32_t
SHAMap::ledgerSeq() const
{
return ledgerSeq_.load(std::memory_order_relaxed);
}
inline SHAMapState
SHAMap::state() const
{
return state_.load(std::memory_order_acquire);
}
inline bool
SHAMap::trySetState(SHAMapState desired) const
{
// Invalid is stored outright: that verdict outranks a concurrent transition.
if (desired == SHAMapState::Invalid)
{
state_.store(SHAMapState::Invalid, std::memory_order_release);
return true;
}
// A failed exchange both reports the state and refreshes expected, so no load is needed
// ahead of the loop.
auto expected = SHAMapState::Modifying;
while (expected != SHAMapState::Invalid)
{
if (state_.compare_exchange_weak(
expected, desired, std::memory_order_acq_rel, std::memory_order_acquire))
{
return true;
}
}
return false;
}
inline bool
SHAMap::setImmutable()
{
XRPL_ASSERT(state_ != SHAMapState::Invalid, "xrpl::SHAMap::setImmutable : state is valid");
state_ = SHAMapState::Immutable;
SOMETIMES(!isValid(), "xrpl::SHAMap::setImmutable : map is invalid");
return trySetState(SHAMapState::Immutable);
}
inline bool
SHAMap::isSynching() const
{
return state_ == SHAMapState::Synching;
return state() == SHAMapState::Synching;
}
inline bool
SHAMap::isImmutable() const
{
return state() == SHAMapState::Immutable;
}
inline void
SHAMap::setSynching()
{
state_ = SHAMapState::Synching;
// Guarded, so this is not a way out of Invalid.
if (!trySetState(SHAMapState::Synching))
{
// Only ever called on a freshly constructed map.
// LCOV_EXCL_START
UNREACHABLE("xrpl::SHAMap::setSynching : map is invalid");
// LCOV_EXCL_STOP
}
}
inline void
SHAMap::clearSynching()
{
state_ = SHAMapState::Modifying;
// Only Synching moves. A walk can end after the ledger settled, and then the map stays
// Immutable; a Modifying map has nothing to clear; an invalid map stays invalid. Only that
// last refusal is logged, since a concurrent walk is what produces it.
auto expected = SHAMapState::Synching;
if (state_.compare_exchange_strong(
expected, SHAMapState::Modifying, std::memory_order_acq_rel, std::memory_order_acquire))
{
return;
}
bool const invalid = expected == SHAMapState::Invalid;
SOMETIMES(invalid, "xrpl::SHAMap::clearSynching : map is invalid");
if (invalid)
{
JLOG(journal_.warn()) << "Refused to clear synching on an invalid map, root hash "
<< root_->getHash();
}
}
inline bool
SHAMap::isValid() const
{
return state_ != SHAMapState::Invalid;
return state() != SHAMapState::Invalid;
}
inline void
SHAMap::setInvalid() const
{
// Through trySetState() like every other transition, so state_ has one writer funnel.
trySetState(SHAMapState::Invalid);
}
inline void
+44 -4
View File
@@ -12,6 +12,10 @@ private:
int bad_;
int duplicate_;
// Whether a node in the batch proved the map impossible. A fact about the batch rather than
// about the map, so a caller can tell this batch's doing from a concurrent walk's verdict.
bool invalidatedMap_;
public:
SHAMapAddNode();
@@ -96,6 +100,26 @@ public:
static SHAMapAddNode
invalid();
/**
* @return A verdict recording one invalid node that proved the map
* impossible.
*/
static SHAMapAddNode
mapInvalidated();
/**
* Whether a node in the batch proved the map impossible.
*
* Reports what the batch did rather than what the map now holds, so a
* verdict another thread's walk reached is not read as this batch's. The
* two differ: SHAMap::setInvalid() is written with no lock held, by a
* getMissingNodes() walk its caller runs lock-free.
*
* @return Whether a node in the batch invalidated the map.
*/
[[nodiscard]] bool
invalidatedMap() const;
/**
* Clear every count back to zero.
*/
@@ -121,15 +145,15 @@ public:
operator+=(SHAMapAddNode const& n);
private:
SHAMapAddNode(int good, int bad, int duplicate);
SHAMapAddNode(int good, int bad, int duplicate, bool invalidatedMap = false);
};
inline SHAMapAddNode::SHAMapAddNode() : good_(0), bad_(0), duplicate_(0)
inline SHAMapAddNode::SHAMapAddNode() : good_(0), bad_(0), duplicate_(0), invalidatedMap_(false)
{
}
inline SHAMapAddNode::SHAMapAddNode(int good, int bad, int duplicate)
: good_(good), bad_(bad), duplicate_(duplicate)
inline SHAMapAddNode::SHAMapAddNode(int good, int bad, int duplicate, bool invalidatedMap)
: good_(good), bad_(bad), duplicate_(duplicate), invalidatedMap_(invalidatedMap)
{
}
@@ -187,6 +211,12 @@ SHAMapAddNode::isUseful() const
return good_ > 0;
}
inline bool
SHAMapAddNode::invalidatedMap() const
{
return invalidatedMap_;
}
inline SHAMapAddNode
SHAMapAddNode::duplicate()
{
@@ -205,10 +235,17 @@ SHAMapAddNode::invalid()
return SHAMapAddNode(0, 1, 0);
}
inline SHAMapAddNode
SHAMapAddNode::mapInvalidated()
{
return SHAMapAddNode(0, 1, 0, true);
}
inline void
SHAMapAddNode::reset()
{
good_ = bad_ = duplicate_ = 0;
invalidatedMap_ = false;
}
inline std::string
@@ -246,6 +283,9 @@ SHAMapAddNode::operator+=(SHAMapAddNode const& n)
bad_ += n.bad_;
duplicate_ += n.duplicate_;
// Or-ed, not summed: one node in the batch having proved the map impossible is the whole fact.
invalidatedMap_ = invalidatedMap_ || n.invalidatedMap_;
return *this;
}
+37 -5
View File
@@ -19,7 +19,8 @@ class SHAMapInnerNode final : public SHAMapTreeNode, public CountedObject<SHAMap
{
public:
/**
* Each inner node has 16 children (the 'radix tree' part of the map)
* Children per inner node: one branch per value of the key nibble that a
* node's depth selects.
*/
static constexpr unsigned int kBranchFactor = 16;
@@ -31,7 +32,25 @@ private:
*/
TaggedPointer hashesAndChildren_;
std::uint32_t fullBelowGen_ = 0;
// Pin that wrapping fullBelowGen_ in an atomic keeps the packed layout, and that the load
// isFullBelow() does once per node of every walk stays lock-free.
static_assert(std::atomic<std::uint32_t>::is_always_lock_free);
static_assert(sizeof(std::atomic<std::uint32_t>) == sizeof(std::uint32_t));
static_assert(alignof(std::atomic<std::uint32_t>) == alignof(std::uint32_t));
/**
* The generation in which a walk found every node below this one resident,
* with this node as the walk's root. The node object is shared by hash
* between maps and positions, and a hash commits to a node's contents
* rather than its place, so the flag is written and read for the root
* position only; below the root, the position-keyed FullBelowCache is the
* memo. Written from more than one thread, since canonicalization shares a
* node between maps and a walk can run with the acquisition lock released
* (see SHAMap::state_). Relaxed both ways, since a generation is only
* compared for equality and the children it vouches for are published
* through lock_.
*/
std::atomic<std::uint32_t> fullBelowGen_ = 0;
std::uint16_t isBranch_ = 0;
/**
@@ -148,10 +167,23 @@ public:
SHAMapTreeNodePtr
canonicalizeChild(unsigned int branch, SHAMapTreeNodePtr node);
// sync functions
/**
* Whether a walk rooted at this node found every node below it resident.
*
* Meaningful only for the node a walk starts at; see fullBelowGen_.
*
* @param generation The FullBelowCache generation the asking walk runs in.
* @return True if a walk rooted here completed in that generation.
*/
bool
isFullBelow(std::uint32_t generation) const;
/**
* Record that a walk rooted at this node found every node below it
* resident.
*
* @param gen The FullBelowCache generation the completing walk ran in.
*/
void
setFullBelowGen(std::uint32_t gen);
@@ -204,13 +236,13 @@ SHAMapInnerNode::getBranchCount() const
inline bool
SHAMapInnerNode::isFullBelow(std::uint32_t generation) const
{
return fullBelowGen_ == generation;
return fullBelowGen_.load(std::memory_order_relaxed) == generation;
}
inline void
SHAMapInnerNode::setFullBelowGen(std::uint32_t gen)
{
fullBelowGen_ = gen;
fullBelowGen_.store(gen, std::memory_order_relaxed);
}
} // namespace xrpl
+37
View File
@@ -75,4 +75,41 @@ leafKey(SHAMapTreeNode const& node)
return safeDowncast<SHAMapLeafNode const&>(node).peekItem()->key();
}
/**
* Whether a node may occupy a position in a SHAMap.
*
* A leaf's own key names its position. An inner node carries no key, so every
* position agrees with it and a caller's own depth rules are what bound it.
*
* @param nodeID the position the node is claimed to occupy.
* @param node the node to judge.
* @return whether the node's own key agrees with that position.
*/
[[nodiscard]] inline bool
belongsAt(SHAMapNodeID const& nodeID, SHAMapTreeNode const& node)
{
return !node.isLeaf() || nodeID.isPrefixOf(leafKey(node));
}
/**
* Whether a node may occupy the child position a branch leads to.
*
* The overload above, asked about a child without building the child's id.
*
* @param parentID the position of the node above, whose depth must be below
* SHAMap::kLeafDepth.
* @param branch the branch of that node leading to the position judged.
* @param node the node to judge.
* @return whether the node's own key agrees with that position.
*/
[[nodiscard]] inline bool
belongsAt(SHAMapNodeID const& parentID, unsigned int branch, SHAMapTreeNode const& node)
{
if (!node.isLeaf())
return true;
auto const& key = leafKey(node);
return parentID.isPrefixOf(key) && selectBranch(parentID, key) == branch;
}
} // namespace xrpl
+51 -2
View File
@@ -123,6 +123,38 @@ operator<<(std::ostream& out, SHAMapNodeID const& node)
return out << to_string(node);
}
/**
* Returns the id of the child a parent's branch leads to
*
* A child's id is its parent's with the nibble at the parent's own depth set to
* the branch taken. Every other nibble is copied through, so a masked parent
* gives a masked child. The write counterpart of selectBranch.
*
* @param parentID the id of the parent. The nibble at parentDepth is
* overwritten rather than merged, so an id already carrying one
* there is accepted.
* @param parentDepth the depth of the parent, below SHAMap::kLeafDepth.
* @param branch the branch of the parent leading to the child.
* @return that child's id, whose depth is parentDepth + 1.
*/
[[nodiscard]] UInt256
childNodeID(UInt256 const& parentID, unsigned int parentDepth, unsigned int branch);
/**
* Whether two keys name the same position at a given depth
*
* Both sides are masked, so either may carry bits below `depth`.
* SHAMapNodeID::isPrefixOf is the stricter form.
*
* @param depth how many leading nibbles name the position, at most
* SHAMap::kLeafDepth. A greater depth is clamped to it.
* @param lhs one of the keys.
* @param rhs the other key.
* @return whether the two agree on those nibbles.
*/
[[nodiscard]] bool
samePositionAtDepth(unsigned int depth, UInt256 const& lhs, UInt256 const& rhs);
/**
* Return an object representing a serialized SHAMap Node ID
*
@@ -144,9 +176,26 @@ deserializeSHAMapNodeID(std::string_view s)
/** @} */
/**
* Returns the branch that would contain the given hash
* Returns the branch at the given depth that would contain the given hash
*
* @param depth the depth of the node whose branch to select.
* @param hash the key whose nibble at that depth names the branch.
* @return the branch containing the hash.
*/
[[nodiscard]] unsigned int
selectBranch(SHAMapNodeID const& id, UInt256 const& hash);
selectBranch(unsigned int depth, UInt256 const& hash);
/**
* Returns the branch that would contain the given hash
*
* @param id the node whose depth to read.
* @param hash the key whose nibble at that depth names the branch.
* @return the branch containing the hash.
*/
[[nodiscard]] inline unsigned int
selectBranch(SHAMapNodeID const& id, UInt256 const& hash)
{
return selectBranch(id.getDepth(), hash);
}
} // namespace xrpl
+16
View File
@@ -375,6 +375,22 @@ public:
/////////////////////////////////////////////////////
// Interface used by AccountDelete
/**
* This method removes one Ticket ledger-object from the ledger. AccountDelete uses it to
* clean up Ticket objects en masse.
*
* This method reduces the TicketCount and the OwnerCount of the Owner AccountRoot object. If
* a TransactionProposal reserved this Ticket, that proposal is deleted as well: once the
* Ticket is gone, its proposed transaction can never be applied (tefNO_TICKET).
*
* @param view the apply view for making changes.
* @param account the account which owns the Ticket object.
* @param ticketIndex ticketIndex is used to construct the ledger-entry ID of Ticket object.
* @param j the journal reference used to log messages.
*
* @return the final TER after all invariant checks. `tefBAD_LEDGER` is returned if Ticket,
* Owner AccountRoot is not found in the apply-view ledger.
*/
static TER
ticketDelete(
ApplyView& view,
@@ -1,7 +1,9 @@
#pragma once
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
@@ -21,6 +23,7 @@
#include <boost/multiprecision/cpp_int.hpp>
#include <cstdint>
#include <map>
#include <set>
#include <string>
#include <tuple>
@@ -363,11 +366,27 @@ public:
* unique to pseudo-accounts. Check that all pseudo-accounts are following the
* rules, and that only pseudo-accounts look like pseudo-accounts.
*
* Post-fixCleanup3_5_0, a pseudo-account also owns only entries that belong
* to its own AMM, Vault or LoanBroker:
* - AMM: its AMM entry, its holdings of the two pool assets, and the trust
* lines of its LP token,
* - Vault: its share MPTokenIssuance, its holding of the vault asset, and the
* LoanBrokers operating on this vault,
* - LoanBroker: its Loans and its holding of the vault asset (the cover).
*
* A pseudo-account cannot sign transactions, so it could never accept or
* remove any other object linked into its owner directory. Such an object
* would pin the directory and block deletion of the owning object.
*/
class ValidPseudoAccounts
{
std::vector<std::string> errors_;
// Entries newly linked into owner directories, keyed by the directory
// owner. Whether an owner is a pseudo-account is decided in finalize,
// where the view is available.
std::map<AccountID, std::vector<uint256>> ownerDirAdditions_;
public:
void
visitEntry(bool, SLE::ConstRef, SLE::ConstRef);
+46 -1
View File
@@ -8,6 +8,7 @@
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Asset.h>
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/Rules.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STTx.h>
@@ -29,7 +30,7 @@ namespace xrpl {
* - 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 (Legacy/CashBasis), or AssetsDeployed (FixedPrecision)
* - assets available do not exceed assets total
* - vault deposit increases assets and share issuance, and adds to:
* total assets, assets available, shares outstanding
@@ -46,6 +47,14 @@ namespace xrpl {
* - vault withdrawal may not succeed when the vault phase is Investment
* - closed-ended loan origination (ttLOAN_SET) may only succeed when the
* vault phase is Investment
* - FixedPrecision only: AssetsDeployed is non-negative and exactly
* representable at the asset's precision; AssetsDeployed changes only for
* ttLOAN_SET, ttLOAN_PAY and ttLOAN_MANAGE; AssetsDeployed is zero right
* after ttVAULT_CREATE and whenever shares outstanding return to zero; the
* stored AssetsTotal cache equals AssetsAvailable + AssetsDeployed, rounded
* Downward to the asset's precision
* - FixedPrecision only: YieldUnrealized is non-negative; YieldUnrealized
* changes only for ttLOAN_SET, ttLOAN_PAY and ttLOAN_MANAGE
*
* Immutability of VaultKind, SubscriptionDate and RedemptionDate is enforced
* by NoModifiedUnmodifiableFields (see InvariantCheck.cpp). From
@@ -68,6 +77,9 @@ class ValidVault
Number assetsAvailable = 0;
Number assetsMaximum = 0;
Number lossUnrealized = 0;
Number assetsDeployed = 0;
Number yieldUnrealized = 0;
VaultVersion version = VaultVersion::Legacy;
std::optional<std::uint8_t> vaultKind;
std::optional<std::uint32_t> subscriptionDate;
std::optional<std::uint32_t> redemptionDate;
@@ -217,6 +229,39 @@ private:
[[nodiscard]] bool
finalizeLoanSet(ReadView const& view, beast::Journal const& j) const;
/**
* @brief Check that a vault's AssetsTotal and AssetsAvailable deltas add
* up against the real transfer, for deposit/withdraw/clawback.
*
* Shared body of the ttVAULT_DEPOSIT, ttVAULT_WITHDRAW and
* ttVAULT_CLAWBACK "assets outstanding"/"assets available" add-up checks,
* which differ only in the rounded/exact deltas plugged in and the verb
* used in the log message.
*
* @param afterVault Snapshot of the vault after the transaction.
* @param beforeVault Snapshot of the vault before the transaction.
* @param vaultAsset The vault's underlying asset.
* @param fix340Enabled Whether fixCleanup3_4_0 is enabled.
* @param minScale The posterior scale computed by computeVaultMinScale.
* @param roundedVaultDelta The real transfer's delta, rounded to minScale.
* @param exactVaultDelta The real transfer's exact (unrounded) delta.
* @param verb The transaction name used in the log message ("deposit",
* "withdrawal" or "clawback").
* @param j Journal for logging.
* @return false if either add-up check fails.
*/
[[nodiscard]] static bool
checkTotalsAddUp(
Vault const& afterVault,
Vault const& beforeVault,
Asset const& vaultAsset,
bool fix340Enabled,
std::int32_t minScale,
Number const& roundedVaultDelta,
Number const& exactVaultDelta,
char const* verb,
beast::Journal const& j);
public:
// Compute the coarsest scale required to represent all numbers
[[nodiscard]] static std::int32_t
@@ -5,6 +5,7 @@
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/Asset.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/XRPAmount.h>
@@ -45,9 +46,29 @@ public:
SLE::Ref loanSle,
SLE::Ref brokerSle,
SLE::Ref vaultSle,
Asset const& vaultAsset,
beast::Journal j);
/**
* @brief Compute the First-Loss Capital cover amount for a defaulted Loan on a FixedPrecision
* Vault, capped at the LoanBroker's CoverAvailable and floored so that AssetsAvailable + cover
* is exactly on its posterior grid.
*
* The LoanBroker absorbs the rounding. When CoverAvailable - cover needs finer digits than the
* broker's posterior grid, it rounds to nearest, as the broker's trust line does, so
* CoverAvailable can move by up to half a posterior ulp more or less than the cover. When the
* two grids differ, no cover is exact on both sides; the Vault side is kept exact.
*
* The one exception: when AssetsAvailable has digits finer than the 16-digit cover can carry,
* no cover cancels them, and the balance writer rounds AssetsAvailable + cover to nearest,
* by at most half a posterior ulp. VaultDeposit and LoanPay credits have the same limit.
* @param loanSle The Loan being defaulted.
* @param brokerSle The Loan's LoanBroker.
* @param vaultSle The LoanBroker's Vault.
* @return The cover amount, in the Vault's Asset.
*/
static STAmount
calculateDefaultCover(SLE::Ref loanSle, SLE::Ref brokerSle, SLE::Ref vaultSle);
/**
* Helper function that might be needed by other transactors
*/
@@ -0,0 +1,44 @@
#pragma once
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/ApplyContext.h>
#include <xrpl/tx/Transactor.h>
namespace xrpl {
class TransactionProposalCancel : public Transactor
{
public:
static constexpr auto kConsequencesFactory = ConsequencesFactoryType::Normal;
explicit TransactionProposalCancel(ApplyContext& ctx) : Transactor(ctx)
{
}
static NotTEC
preflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
void
visitInvariantEntry(bool isDelete, SLE::ConstRef before, SLE::ConstRef after) override;
[[nodiscard]] bool
finalizeInvariants(
STTx const& tx,
TER result,
XRPAmount fee,
ReadView const& view,
beast::Journal const& j) override;
};
} // namespace xrpl
@@ -52,6 +52,13 @@ private:
TER
applyFee();
/**
* Write the default Feature Extension fee settings into FeeSettings on
* `featureSmartEscrow` activation.
*/
void
initializeVMFees();
TER
applyUNLModify();
};
+92 -15
View File
@@ -200,11 +200,23 @@ Ledger::Ledger(
sle->at(sfReserveIncrement) = *f;
sle->at(sfReferenceFeeUnits) = kFeeUnitsDeprecated;
}
if (std::ranges::find(amendments, featureSmartEscrow) != amendments.end())
{
sle->at(sfGasLimit) = fees.gasLimit;
sle->at(sfBytecodeSizeLimit) = fees.bytecodeSizeLimit;
sle->at(sfGasPrice) = fees.gasPrice;
}
rawInsert(sle);
}
stateMap_.flushDirty(NodeObjectType::AccountNode);
setImmutable();
// Built locally. See Ledger::setImmutable().
if (!setImmutable())
{
// LCOV_EXCL_START
logicError("Ledger::Ledger(CreateGenesisT, ...): genesis ledger map is invalid");
// LCOV_EXCL_STOP
}
}
Ledger::Ledger(
@@ -215,7 +227,7 @@ Ledger::Ledger(
Fees const& fees,
Family& family,
beast::Journal j)
: immutable_(true)
: immutable_(false)
, txMap_(SHAMapType::TRANSACTION, info.txHash, family)
, stateMap_(SHAMapType::STATE, info.accountHash, family)
, fees_(fees)
@@ -238,8 +250,20 @@ Ledger::Ledger(
JLOG(j.warn()) << "Don't have state data root for ledger" << header_.seq;
}
txMap_.setImmutable();
stateMap_.setImmutable();
// Loaded locally. See Ledger::setImmutable().
if (setMapsImmutable())
{
immutable_ = true;
}
else
{
// LCOV_EXCL_START
JLOG(j.error()) << "Invalid map for ledger " << header_.seq;
UNREACHABLE("xrpl::Ledger::Ledger(LedgerHeader const&, ...) : map is invalid");
// Treat it as a damaged ledger: the code below recomputes the hash and re-acquires.
loaded = false;
// LCOV_EXCL_STOP
}
if (!setup())
loaded = false;
@@ -281,7 +305,8 @@ Ledger::Ledger(Ledger const& prevLedger, NetClock::time_point closeTime)
}
Ledger::Ledger(LedgerHeader const& info, Rules rules, Family& family)
: immutable_(true)
: immutable_(false)
, mapHashesFromHeader_(true)
, txMap_(SHAMapType::TRANSACTION, info.txHash, family)
, stateMap_(SHAMapType::STATE, info.accountHash, family)
, rules_(std::move(rules))
@@ -310,27 +335,48 @@ Ledger::Ledger(
setup();
}
void
bool
Ledger::setImmutable(bool rehash)
{
// Force update, since this is the only
// place the hash transitions to valid
if (!immutable_ && rehash)
// An immutable ledger is treated as persistable, so an invalid map must never become
// immutable. The validity test runs before anything is written, so a refusal leaves
// the header as it was.
if (!mapsValid())
return false;
// Read now but written to the header below: getHash() can unshare a dirty tree, so it must run
// while the map is still mutable, while the header may only be written once both maps are
// settled. Skipped when the ledger is already immutable or when the header supplied these.
bool const deriveMapHashes = !immutable_ && !mapHashesFromHeader_ && rehash;
UInt256 const txHash = deriveMapHashes ? txMap_.getHash().asUInt256() : UInt256{};
UInt256 const accountHash = deriveMapHashes ? stateMap_.getHash().asUInt256() : UInt256{};
// A concurrent walk can invalidate a map between the check above and here (see
// SHAMap::state_), so the result is checked. That narrows the window rather than closing it,
// since setInvalid() outranks Immutable.
bool const bothImmutable = setMapsImmutable();
SOMETIMES(!bothImmutable, "xrpl::Ledger::setImmutable : map invalidated while going immutable");
if (!bothImmutable)
return false; // LCOV_EXCL_LINE: only the walk named above reaches this, so no test does
// Written only now, so failing the check above leaves the header describing what the ledger was
// built from. Forced, since this is the only place the hash transitions to valid.
if (deriveMapHashes)
{
header_.txHash = txMap_.getHash().asUInt256();
header_.accountHash = stateMap_.getHash().asUInt256();
header_.txHash = txHash;
header_.accountHash = accountHash;
}
if (rehash)
header_.hash = calculateLedgerHash(header_);
// Set last, so isImmutable() reports only a ledger whose maps are both immutable.
immutable_ = true;
txMap_.setImmutable();
stateMap_.setImmutable();
setup();
return true;
}
void
bool
Ledger::setAccepted(
NetClock::time_point closeTime,
NetClock::duration closeResolution,
@@ -342,7 +388,17 @@ Ledger::setAccepted(
header_.closeTime = closeTime;
header_.closeTimeResolution = closeResolution;
header_.closeFlags = correctCloseTime ? 0 : kSLcfNoConsensusTime;
setImmutable();
// Built locally. See Ledger::setImmutable().
if (!setImmutable())
{
// LCOV_EXCL_START
JLOG(j_.error()) << "Invalid map for accepted ledger " << header_.seq;
UNREACHABLE("xrpl::Ledger::setAccepted : map is invalid");
return false;
// LCOV_EXCL_STOP
}
return true;
}
bool
@@ -566,6 +622,7 @@ Ledger::setup()
{
bool oldFees = false;
bool newFees = false;
bool extensionFees = false;
{
auto const baseFee = sle->at(~sfBaseFee);
auto const reserveBase = sle->at(~sfReserveBase);
@@ -600,6 +657,20 @@ Ledger::setup()
assign(fees_.increment, reserveIncrementXRP);
newFees = baseFeeXRP || reserveBaseXRP || reserveIncrementXRP;
}
{
auto const gasLimit = sle->at(~sfGasLimit);
auto const bytecodeSizeLimit = sle->at(~sfBytecodeSizeLimit);
auto const gasPrice = sle->at(~sfGasPrice);
// An absent field resolves to the protocol constant.
if (rules_.enabled(featureSmartEscrow))
{
fees_.gasLimit = gasLimit.value_or(kDefaultGasLimit);
fees_.bytecodeSizeLimit = bytecodeSizeLimit.value_or(kDefaultBytecodeSizeLimit);
fees_.gasPrice = gasPrice.value_or(kDefaultGasPrice);
}
extensionFees = gasLimit || bytecodeSizeLimit || gasPrice;
}
if (oldFees && newFees)
{
// Should be all of one or the other, but not both
@@ -610,6 +681,12 @@ Ledger::setup()
// Can't populate the new fees before the amendment is enabled
ret = false;
}
if (!rules_.enabled(featureSmartEscrow) && extensionFees)
{
// Can't populate the extension fees before the amendment is
// enabled
ret = false;
}
}
}
catch (SHAMapMissingNode const&)
-2
View File
@@ -435,8 +435,6 @@ PaymentSandbox::issuerSelfDebitHookMPT(
std::uint64_t amount,
std::int64_t origBalance)
{
XRPL_ASSERT(amount > 0, "PaymentSandbox::issuerSelfDebitHookMPT: amount must be > 0");
tab_.issuerSelfDebitMPT(issue, amount, origBalance);
}
+29 -7
View File
@@ -511,9 +511,14 @@ doWithdraw(
AccountID const& dstAcct,
AccountID const& sourceAcct,
XRPAmount priorBalance,
STAmount const& amount,
STAmount const& destinationAmount,
STAmount const& sourceAmount,
beast::Journal j)
{
XRPL_ASSERT(
destinationAmount.asset() == sourceAmount.asset(),
"xrpl::doWithdraw : delivered and source amounts use the same asset");
auto const dstSle = ctx.view.read(keylet::account(dstAcct));
// Create a trust line or MPToken for a self-destination only when there
@@ -523,9 +528,10 @@ doWithdraw(
// create+delete MPTokens in the same transaction.
if (dstAcct == senderAcct)
{
if (amount > beast::kZero || !ctx.view.rules().enabled(fixCleanup3_4_0))
if (destinationAmount > beast::kZero || !ctx.view.rules().enabled(fixCleanup3_4_0))
{
if (auto const ter = addEmptyHolding(ctx, senderAcct, priorBalance, amount.asset(), j);
if (auto const ter =
addEmptyHolding(ctx, senderAcct, priorBalance, destinationAmount.asset(), j);
!isTesSuccess(ter) && ter != tecDUPLICATE)
return ter;
}
@@ -540,17 +546,33 @@ doWithdraw(
if (accountHolds(
ctx.view,
sourceAcct,
amount.asset(),
destinationAmount.asset(),
FreezeHandling::IgnoreFreeze,
AuthHandling::IgnoreAuth,
j) < amount)
j) < sourceAmount)
{
// LCOV_EXCL_START
JLOG(j.error()) << "doWithdraw: negative balance of broker cover assets.";
JLOG(j.error()) << "doWithdraw: source account holds fewer assets than the withdrawal.";
return tefINTERNAL;
// LCOV_EXCL_STOP
}
// A transfer fee applies. Redeem the gross amount with the issuer, then
// issue the net amount to the destination.
if (sourceAmount != destinationAmount)
{
auto const issuer = destinationAmount.getIssuer();
XRPL_ASSERT(
!destinationAmount.native() && sourceAcct != issuer && dstAcct != issuer,
"xrpl::doWithdraw : transfer fee applies between token holders");
if (auto const ter = directSendNoFee(ctx.view, sourceAcct, issuer, sourceAmount, false, j);
!isTesSuccess(ter))
return ter; // LCOV_EXCL_LINE
return directSendNoFee(ctx.view, issuer, dstAcct, destinationAmount, false, j);
}
// A reserve sponsor only covers tx.Account's own objects, so resolve the
// sponsor against the destination. accountSend can auto-create a holding
// for dstAcct; keying on the destination ensures a third-party destination's
@@ -562,7 +584,7 @@ doWithdraw(
// Move the funds directly from the broker's pseudo-account to the
// dstAcct
return accountSend(
ctx.view, sourceAcct, dstAcct, amount, j, *sponsorSle, WaiveTransferFee::Yes);
ctx.view, sourceAcct, dstAcct, destinationAmount, j, *sponsorSle, WaiveTransferFee::Yes);
}
TER
+4 -34
View File
@@ -213,49 +213,19 @@ adjustAmountsByLPTokens(
if (lpTokensActual < lpTokens)
{
bool const ammRoundingEnabled = [&] {
if (auto const& rules = getCurrentTransactionRules();
rules && rules->enabled(fixAMMv1_1))
return true;
return false;
}();
// Equal trade
if (amount2)
{
Number const fr = lpTokensActual / lpTokens;
auto const amountActual = toSTAmount(amount.asset(), fr * amount);
auto const amount2Actual = toSTAmount(amount2->asset(), fr * *amount2);
if (!ammRoundingEnabled)
{
return std::make_tuple(
amountActual < amount ? amountActual : amount,
amount2Actual < amount2 ? amount2Actual : amount2,
lpTokensActual);
}
return std::make_tuple(amountActual, amount2Actual, lpTokensActual);
}
// Single trade
auto const amountActual = [&] {
if (isDeposit == IsDeposit::Yes)
{
return ammAssetIn(amountBalance, lptAMMBalance, lpTokensActual, tfee);
}
if (!ammRoundingEnabled)
{
return ammAssetOut(amountBalance, lptAMMBalance, lpTokens, tfee);
}
return ammAssetOut(amountBalance, lptAMMBalance, lpTokensActual, tfee);
}();
if (!ammRoundingEnabled)
{
return amountActual < amount
? std::make_tuple(amountActual, std::nullopt, lpTokensActual)
: std::make_tuple(amount, std::nullopt, lpTokensActual);
}
auto const amountActual = isDeposit == IsDeposit::Yes
? ammAssetIn(amountBalance, lptAMMBalance, lpTokensActual, tfee)
: ammAssetOut(amountBalance, lptAMMBalance, lpTokensActual, tfee);
return std::make_tuple(amountActual, std::nullopt, lpTokensActual);
}
@@ -980,7 +950,7 @@ verifyAndAdjustLPTokenBalance(
}
else
{
return std::unexpected<TER>(tecAMM_INVALID_TOKENS);
return std::unexpected<TER>(tecAMM_INVALID_TOKENS); // LCOV_EXCL_LINE
}
}
return true;
+159 -5
View File
@@ -66,6 +66,161 @@ canApplyToBrokerCover(
return tesSUCCESS;
}
namespace detail {
[[nodiscard]] int
getPosteriorBrokerCoverScale(SLE::ConstRef vault, SLE::ConstRef broker, Number const& delta)
{
XRPL_ASSERT(
vault && vault->getType() == ltVAULT,
"xrpl::detail::getPosteriorBrokerCoverScale : valid Vault sle");
XRPL_ASSERT(
broker && broker->getType() == ltLOAN_BROKER,
"xrpl::detail::getPosteriorBrokerCoverScale : valid LoanBroker sle");
return posteriorAssetScale(
getVaultVersion(vault),
vault->at(sfAsset),
getVaultBaseScale(vault),
broker->at(sfCoverAvailable),
delta);
}
[[nodiscard]] STAmount
roundToPosteriorBrokerCoverScale(
SLE::ConstRef vault,
SLE::ConstRef broker,
STAmount const& delta,
Number::RoundingMode roundingMode)
{
XRPL_ASSERT(
vault && vault->getType() == ltVAULT,
"xrpl::detail::roundToPosteriorBrokerCoverScale : valid Vault sle");
XRPL_ASSERT(
broker && broker->getType() == ltLOAN_BROKER,
"xrpl::detail::roundToPosteriorBrokerCoverScale : valid LoanBroker sle");
XRPL_ASSERT(
delta.asset() == vault->at(sfAsset),
"xrpl::detail::roundToPosteriorBrokerCoverScale : delta and Vault asset match");
if (delta.integral())
return delta;
return roundToScale(delta, getPosteriorBrokerCoverScale(vault, broker, delta), roundingMode);
}
} // namespace detail
[[nodiscard]] STAmount
creditToPosteriorBrokerCoverScale(
SLE::ConstRef vault,
SLE::ConstRef broker,
Number const& raw,
Number::RoundingMode roundingMode)
{
XRPL_ASSERT(
vault && vault->getType() == ltVAULT,
"xrpl::creditToPosteriorBrokerCoverScale : valid Vault sle");
XRPL_ASSERT(
broker && broker->getType() == ltLOAN_BROKER,
"xrpl::creditToPosteriorBrokerCoverScale : valid LoanBroker sle");
Asset const asset = vault->at(sfAsset);
if (asset.integral())
{
NumberRoundModeGuard const rg(roundingMode);
return STAmount{asset, raw};
}
Number const reference = broker->at(sfCoverAvailable);
return detail::creditToPosteriorScale(
asset, reference, getVaultBaseScale(vault), raw, roundingMode);
}
[[nodiscard]] STAmount
debitToPosteriorBrokerCoverScale(
SLE::ConstRef vault,
SLE::ConstRef broker,
STAmount const& amount,
Number::RoundingMode roundingMode)
{
XRPL_ASSERT(
vault && vault->getType() == ltVAULT,
"xrpl::debitToPosteriorBrokerCoverScale : valid Vault sle");
XRPL_ASSERT(
broker && broker->getType() == ltLOAN_BROKER,
"xrpl::debitToPosteriorBrokerCoverScale : valid LoanBroker sle");
XRPL_ASSERT(
amount.asset() == vault->at(sfAsset),
"xrpl::debitToPosteriorBrokerCoverScale : amount and Vault asset match");
XRPL_ASSERT(!amount.negative(), "xrpl::debitToPosteriorBrokerCoverScale : non-negative amount");
return -detail::roundToPosteriorBrokerCoverScale(vault, broker, -amount, roundingMode);
}
[[nodiscard]] TER
checkOptionalBrokerCoverInflow(SLE::ConstRef vault, SLE::ConstRef broker, STAmount const& amount)
{
XRPL_ASSERT(
vault && vault->getType() == ltVAULT,
"xrpl::checkOptionalBrokerCoverInflow : valid Vault sle");
XRPL_ASSERT(
broker && broker->getType() == ltLOAN_BROKER,
"xrpl::checkOptionalBrokerCoverInflow : valid LoanBroker sle");
XRPL_ASSERT(
amount.asset() == vault->at(sfAsset),
"xrpl::checkOptionalBrokerCoverInflow : amount and Vault asset match");
XRPL_ASSERT(!amount.negative(), "xrpl::checkOptionalBrokerCoverInflow : non-negative amount");
if (getVaultVersion(vault) != VaultVersion::FixedPrecision)
return tesSUCCESS;
// amount is the effective credit (already floored on the posterior
// CoverAvailable grid by creditToPosteriorBrokerCoverScale); rounding it
// again as a standalone delta could hide a crossing.
STAmount const& rounded = amount;
int const baseScale = getVaultBaseScale(vault);
// Keep this explicit even though the Open-limit capacity check below rejects
// every coarsening transition too. The protocol defines both conditions
// independently.
if (detail::getPosteriorBrokerCoverScale(vault, broker, rounded) != baseScale)
return tecLIMIT_EXCEEDED;
Number const posterior = [&] {
NumberRoundModeGuard const rg(Number::RoundingMode::TowardsZero);
return broker->at(sfCoverAvailable) + rounded;
}();
if (posterior > getVaultOpenLimit(vault))
return tecLIMIT_EXCEEDED;
return tesSUCCESS;
}
void
adjustBrokerDebtTotal(
SLE::Ref brokerSle,
SLE::ConstRef vaultSle,
Number const& delta,
int vaultScale)
{
// On FixedPrecision Vaults, LoanSet adds principal to DebtTotal exactly, LoanPay and default
// subtract exact amounts. Thus DebtTotal must not be rounded coarser than the base scale.
if (getVaultVersion(vaultSle) == VaultVersion::FixedPrecision)
{
brokerSle->at(sfDebtTotal) += delta;
}
else
{
adjustImpreciseNumber(brokerSle->at(sfDebtTotal), delta, vaultSle->at(sfAsset), vaultScale);
}
}
Number
minimumBrokerCover(Number const& debtTotal, TenthBips32 coverRateMinimum, SLE::ConstRef vaultSle)
{
XRPL_ASSERT(
vaultSle && vaultSle->getType() == ltVAULT, "xrpl::minimumBrokerCover : valid Vault sle");
NumberRoundModeGuard const mg(Number::RoundingMode::Upward);
return roundToAsset(
vaultSle->at(sfAsset),
tenthBipsOfValue(debtTotal, coverRateMinimum),
getVaultBaseScale(vaultSle));
}
bool
checkLendingProtocolDependencies(Rules const& rules, STTx const& tx)
{
@@ -250,14 +405,13 @@ loanPaymentDeltas(LoanPaymentParts const& parts)
namespace {
// Cash-basis accounting applies only when featureLendingProtocolV1_1 is
// enabled AND the specific Vault was created under it (LEVersion ==
// VaultVersion::CashBasis). Vaults created before activation keep instant
// interest recognition forever, even after the amendment later turns on.
// Cash-basis accounting applies to Vaults created under
// featureLendingProtocolV1_1 or a later version. Vaults created before
// activation keep instant interest recognition forever.
bool
cashBasisEnabled(SLE::ConstRef vaultSle)
{
return getVaultVersion(vaultSle) == VaultVersion::CashBasis;
return getVaultVersion(vaultSle) >= VaultVersion::CashBasis;
}
} // namespace
@@ -1047,6 +1047,14 @@ maxMPTAmount(SLE const& sleIssuance)
return sleIssuance[~sfMaximumAmount].value_or(kMaxMpTokenAmount);
}
std::optional<std::int64_t>
maxMPTAmount(ReadView const& view, MPTID const& mptID)
{
if (auto const sle = view.read(keylet::mptokenIssuance(mptID)))
return maxMPTAmount(*sle);
return std::nullopt;
}
std::int64_t
availableMPTAmount(SLE const& sleIssuance)
{
+157 -5
View File
@@ -1,18 +1,51 @@
#include <xrpl/ledger/helpers/ProposalHelpers.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/Slice.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Keylet.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STArray.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/Serializer.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/TxFormats.h>
#include <algorithm>
#include <array>
#include <cstdint>
#include <optional>
namespace xrpl::proposal {
namespace {
// Proposals do not nest, so a proposed transaction may carry none of these
// types, neither as its own payload nor as a Batch inner transaction.
constexpr auto kProposalTxTypes = std::to_array<TxType>({
ttTRANSACTION_PROPOSAL_CREATE,
ttTRANSACTION_PROPOSAL_CANCEL,
});
} // namespace
bool
isValidProposal(STObject const& proposedTx)
isValidProposalTxnType(STObject const& proposedTx)
{
if (isProposalTx(proposedTx))
auto const type = proposedTx.getFieldU16(sfTransactionType);
if (std::ranges::find(kProposalTxTypes, type) != kProposalTxTypes.end())
return false;
if (isPseudoTx(proposedTx))
@@ -22,13 +55,24 @@ isValidProposal(STObject const& proposedTx)
(proposedTx.getFieldU32(sfFlags) & tfInnerBatchTxn) != 0u)
return false;
if (proposedTx.getFieldU16(sfTransactionType) == ttBATCH &&
proposedTx.isFieldPresent(sfRawTransactions))
if (type == ttBATCH && proposedTx.isFieldPresent(sfRawTransactions))
{
STArray const& innerTxns = proposedTx.getFieldArray(sfRawTransactions);
for (STObject const& inner : innerTxns)
{
if (isProposalTx(inner) || isPseudoTx(inner))
// The only production caller reaches this walk through STTx
// construction, which rejects a typeless inner first, so this is
// re-checked rather than relied upon.
if (!inner.isFieldPresent(sfTransactionType))
return false;
auto const innerType = inner.getFieldU16(sfTransactionType);
// A nested Batch is rejected alongside nested proposals and
// pseudo-transactions: STTx construction forbids inner Batches, so
// a proposal must not be able to store one either.
if (std::ranges::find(kProposalTxTypes, innerType) != kProposalTxTypes.end() ||
innerType == ttBATCH || isPseudoTx(inner))
return false;
}
}
@@ -36,4 +80,112 @@ isValidProposal(STObject const& proposedTx)
return true;
}
bool
isTerminal(
ReadView const& view,
std::optional<std::uint32_t> expiration,
STObject const& proposedTx)
{
if (hasExpired(view, expiration))
return true;
// Mirror the ordinary transactor's tefMAX_LEDGER rule: view.seq() > bound.
// At view.seq() == bound the payload can still be submitted, so the
// proposal is still live (On-Chain Cosigner spec §4.5).
return proposedTx.isFieldPresent(sfLastLedgerSequence) &&
view.seq() > proposedTx.getFieldU32(sfLastLedgerSequence);
}
TER
deleteProposal(ApplyView& view, SLE::pointer const& sleProposal, beast::Journal j)
{
if (!sleProposal || sleProposal->getType() != ltTRANSACTION_PROPOSAL ||
!view.exists(Keylet{ltTRANSACTION_PROPOSAL, sleProposal->key()}))
{
// LCOV_EXCL_START
JLOG(j.fatal()) << "Invalid TransactionProposal deletion.";
return tecINTERNAL;
// LCOV_EXCL_STOP
}
AccountID const owner = sleProposal->getAccountID(sfOwner);
std::uint64_t const page{(*sleProposal)[sfOwnerNode]};
if (!view.dirRemove(keylet::ownerDir(owner), page, sleProposal->key(), true))
{
// LCOV_EXCL_START
JLOG(j.fatal()) << "Unable to delete TransactionProposal from owner.";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
auto const sleOwner = view.peek(keylet::account(owner));
if (!sleOwner)
{
// LCOV_EXCL_START
JLOG(j.fatal()) << "Could not find TransactionProposal owner account root.";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
// Release the reserve against the Owner or, if the entry carries a
// reserve sponsor, against that sponsor.
decreaseOwnerCountForObject(
view,
sleOwner,
sleProposal,
proposalOwnerCount(sleProposal->getFieldObject(sfProposedTransaction)),
j);
view.erase(sleProposal);
return tesSUCCESS;
}
bool
payloadMatches(STObject const& proposedTx, STObject const& tx)
{
XRPL_ASSERT(
proposedTx.isFieldPresent(sfTransactionType) && tx.isFieldPresent(sfTransactionType),
"xrpl::proposal::payloadMatches : transaction-shaped inputs");
// The signing-payload serialization omits every kNotSigning field: the
// signature containers a proposal lets evolve (TxnSignature, Signers,
// BatchSigners, CounterpartySignature, SponsorSignature) plus Signature
// and MasterSignature, which the account's own signature does not cover
// either (see the header). What remains is the field set fixed at
// creation — except SigningPubKey, which signing payloads include but
// which is also mutable here (stored empty, filled if the target signs
// with its own key). Neutralize it on both sides before comparing.
auto const payloadBytes = [](STObject const& obj) {
STObject copy{obj};
copy.setFieldVL(sfSigningPubKey, Slice{});
Serializer s;
copy.addWithoutSigningFields(s);
return s.getData();
};
return payloadBytes(proposedTx) == payloadBytes(tx);
}
bool
canConsumeTicket(ReadView const& view, STTx const& tx)
{
auto const seqProx = tx.getSeqProxy();
if (!seqProx.isTicket())
{
// The caller only reaches here for a Ticket-based transaction.
// LCOV_EXCL_START
UNREACHABLE("xrpl::proposal::canConsumeTicket : tx spends a Sequence");
return false;
// LCOV_EXCL_STOP
}
auto const sleProposal =
view.read(keylet::txProposal(tx.getAccountID(sfAccount), seqProx.value()));
if (!sleProposal)
return true;
return payloadMatches(sleProposal->getFieldObject(sfProposedTransaction), tx);
}
} // namespace xrpl::proposal
+24 -17
View File
@@ -2,6 +2,7 @@
#include <xrpl/basics/Log.h>
#include <xrpl/basics/MathUtilities.h>
#include <xrpl/basics/Number.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/beast/utility/instrumentation.h>
@@ -551,6 +552,22 @@ transferRate(ReadView const& view, STAmount const& amount)
return transferRate(view, amount.asset());
}
STAmount
addTransferFee(STAmount const& amount, Rate const& rate)
{
if (rate == kParityRate)
return amount;
return mulRatio(amount, rate.value, QUALITY_ONE, Number::RoundingMode::Upward);
}
STAmount
subtractTransferFee(STAmount const& amount, Rate const& rate)
{
if (rate == kParityRate)
return amount;
return mulRatio(amount, QUALITY_ONE, rate.value, Number::RoundingMode::Downward);
}
//------------------------------------------------------------------------------
//
// Holding operations
@@ -973,19 +990,11 @@ accountSendIOU(
SLE::Ref sponsorSle,
WaiveTransferFee waiveFee)
{
if (view.rules().enabled(fixAMMv1_1))
{
if (saAmount < beast::kZero || saAmount.holds<MPTIssue>())
{
return tecINTERNAL; // LCOV_EXCL_LINE
}
}
else
if (saAmount < beast::kZero || saAmount.holds<MPTIssue>())
{
// LCOV_EXCL_START
XRPL_ASSERT(
saAmount >= beast::kZero && !saAmount.holds<MPTIssue>(),
"xrpl::accountSendIOU : minimum amount and not MPT");
JLOG(j.fatal()) << "accountSendIOU: negative amount or invalid asset";
return tecINTERNAL;
// LCOV_EXCL_STOP
}
@@ -1355,9 +1364,8 @@ directSendNoLimitMPT(
{
// Number math loses precision on large MPT amounts, which can
// overcharge the sender. MPTs are integral, so compute the cost
// exactly and round it up, matching the payment engine.
auto const cost = mulRatio(saAmount.mpt(), rate.value, QUALITY_ONE, true);
saActual = STAmount(saAmount.asset(), cost.value());
// exactly, matching the payment engine.
saActual = addTransferFee(saAmount, rate);
}
else
{
@@ -1482,9 +1490,8 @@ directSendNoLimitMultiMPT(
{
// Number math loses precision on large MPT amounts, which can
// overcharge the sender. MPTs are integral, so compute the
// cost exactly and round it up, matching the payment engine.
auto const cost = mulRatio(amount.mpt(), rate.value, QUALITY_ONE, true);
actualSend = STAmount(amount.asset(), cost.value());
// cost exactly, matching the payment engine.
actualSend = addTransferFee(amount, rate);
}
else
{
+86 -26
View File
@@ -25,6 +25,8 @@
#include <cstdint>
#include <expected>
#include <optional>
#include <stdexcept>
#include <string_view>
#include <utility>
namespace xrpl {
@@ -46,7 +48,7 @@ fixedBaseScale(SLE::ConstRef vault)
}
// Thin, vault-specific wrapper over posteriorAssetScale: used by
// getPosteriorVaultScale and creditToPosteriorAvailableScale.
// getPosteriorVaultScale and the cash outflow rounding below.
[[nodiscard]] int
posteriorScale(SLE::ConstRef vault, Number const& reference, Number const& delta)
{
@@ -134,6 +136,18 @@ syncAssetsTotal(SLE::Ref vault)
vault->at(sfAssetsTotal) = STAmount{asset, getAssetsTotal(vault)};
}
[[nodiscard]] bool
debitIsNonZeroDust(SLE::ConstRef vault, Number const& amount)
{
if (amount == 0)
return false;
auto const total = getVaultVersion(vault) == VaultVersion::FixedPrecision
? vault->at(sfAssetsAvailable)
: getAssetsTotal(vault);
auto const asset = vault->at(sfAsset);
return STAmount{asset, total - amount} == STAmount{asset, total};
}
} // namespace
namespace detail {
@@ -181,11 +195,11 @@ posteriorAssetScale(
creditToPosteriorScale(
Asset const& asset,
Number const& reference,
int atScale,
int baseScale,
Number const& raw,
Number::RoundingMode roundingMode)
{
// Floor the SUM (reference + raw), not just raw, at atScale. See
// Floor the SUM (reference + raw), not just raw. See
// creditToPosteriorAvailableScale's doc: a delta floored on its own grid
// can still leave a 17-digit sum once the reference has crossed a power
// of ten.
@@ -194,9 +208,21 @@ creditToPosteriorScale(
// than Number holds (the sum is on the posterior grid, the reference on
// the finer one). Compute both under roundingMode, not the caller's
// ambient mode, so the credit this returns never exceeds raw.
//
// Take the grid from this same sum. A sum just under a power of ten that
// rounds up to it in another mode would pick the coarser grid, and
// flooring the unrounded sum on it would land below reference.
NumberRoundModeGuard const rg(roundingMode);
Number const flooredSum = roundToAsset(asset, reference + raw, atScale, roundingMode);
return STAmount{asset, flooredSum - reference};
Number const sum = reference + raw;
int const atScale = liveScale(sum, asset, baseScale);
Number const flooredSum = roundToAsset(asset, sum, atScale, roundingMode);
STAmount const credit{asset, flooredSum - reference};
XRPL_ASSERT_PARTS(
roundingMode != Number::RoundingMode::Downward ||
(credit >= beast::kZero && Number(credit) <= raw),
"xrpl::detail::creditToPosteriorScale",
"credit is between zero and raw");
return credit;
}
[[nodiscard]] int
@@ -302,6 +328,17 @@ adjustVaultBalances(SLE::Ref vault, VaultBalanceChange const& change, beast::Jou
return tesSUCCESS;
}
[[nodiscard]] TER
adjustVaultCash(SLE::Ref vault, STAmount const& delta, beast::Journal j)
{
if (getVaultVersion(vault) == VaultVersion::FixedPrecision)
return adjustVaultBalances(vault, {.cash = delta}, j);
vault->at(sfAssetsTotal) += delta;
vault->at(sfAssetsAvailable) += delta;
return tesSUCCESS;
}
[[nodiscard]] int
getVaultScale(SLE::ConstRef vault)
{
@@ -374,8 +411,8 @@ creditToPosteriorAvailableScale(
}
Number const reference = vault->at(sfAssetsAvailable);
int const scale = posteriorScale(vault, reference, raw);
return detail::creditToPosteriorScale(asset, reference, scale, raw, roundingMode);
return detail::creditToPosteriorScale(
asset, reference, fixedBaseScale(vault), raw, roundingMode);
}
[[nodiscard]] Number
@@ -428,6 +465,36 @@ vaultOpenZoneCapacity(SLE::ConstRef vault, Number const& roundedAmount)
return getAssetsTotal(vault) + vault->at(sfYieldUnrealized) + roundedAmount;
}
[[nodiscard]] TER
checkDebitNotDust(
SLE::ConstRef vault,
Number const& amount,
beast::Journal j,
std::string_view name)
{
try
{
if (debitIsNonZeroDust(vault, amount))
{
JLOG(j.debug()) << name << ": amount too small to change stored vault balance";
return tecPRECISION_LOSS;
}
}
// LCOV_EXCL_START
catch (std::overflow_error const&)
{
// It's easy to hit this exception from Number with large enough Scale
// so we avoid spamming the log and only use debug here.
JLOG(j.debug()) //
<< name << ": overflow error with"
<< " scale=" << (int)vault->at(sfScale) //
<< ", assetsTotal=" << vault->at(sfAssetsTotal) << ", amount=" << amount;
return tecPATH_DRY;
}
// LCOV_EXCL_STOP
return tesSUCCESS;
}
[[nodiscard]] std::optional<STAmount>
assetsToSharesDeposit(SLE::ConstRef vault, SLE::ConstRef issuance, STAmount const& assets)
{
@@ -575,6 +642,14 @@ isOnVaultBaseGrid(Asset const& asset, Number const& value, int baseScale)
roundToAsset(asset, value, baseScale, Number::RoundingMode::TowardsZero) == value;
}
[[nodiscard]] TER
checkAssetsMaximum(SLE::ConstRef vault, Number const& amount)
{
return isOnVaultBaseGrid(vault->at(sfAsset), amount, getVaultBaseScale(vault))
? TER{tesSUCCESS}
: TER{tecPRECISION_LOSS};
}
[[nodiscard]] Number
assetsTotalForWithdrawal(SLE::ConstRef vault, WaiveUnrealizedLoss waive)
{
@@ -584,22 +659,6 @@ assetsTotalForWithdrawal(SLE::ConstRef vault, WaiveUnrealizedLoss waive)
return assetTotal;
}
[[nodiscard]] bool
debitIsNonZeroDust(Asset const& asset, Number const& total, Number const& amount)
{
if (amount == 0)
return false;
return STAmount{asset, total - amount} == STAmount{asset, total};
}
[[nodiscard]] Number
vaultDebitDustReference(SLE::ConstRef vault, Number const& assetsTotal)
{
return getVaultVersion(vault) == VaultVersion::FixedPrecision
? Number(vault->at(sfAssetsAvailable))
: assetsTotal;
}
[[nodiscard]] std::optional<STAmount>
assetsToSharesWithdraw(
SLE::ConstRef vault,
@@ -695,9 +754,10 @@ getVaultVersion(SLE::ConstRef vault)
[[nodiscard]] VaultVersion
getVaultDefaultVersion(Rules const& rules)
{
// FixedPrecision requires both cleanups, so a FixedPrecision Vault always
// sees them enabled.
if (rules.enabled(featureLendingProtocolV1_2) && rules.enabled(fixCleanup3_2_0) &&
// FixedPrecision requires featureLendingProtocolV1_1 (closed-ended Vaults and
// cash-basis accounting) and fixCleanup3_4_0, so a FixedPrecision Vault always
// sees both enabled. fixCleanup3_2_0 is already enabled on the network.
if (rules.enabled(featureLendingProtocolV1_1) && rules.enabled(featureLendingProtocolV1_2) &&
rules.enabled(fixCleanup3_4_0))
return VaultVersion::FixedPrecision;
if (rules.enabled(featureLendingProtocolV1_1))
+12 -5
View File
@@ -435,18 +435,25 @@ areMirrorsCurrent(SLE const& issuance, SLE const& mptoken)
}
void
setIssuerMirrorEpoch(SLE const& issuance, SLE& mptoken)
setIssuerMirrorCurrent(SLE const& issuance, SLE& mptoken)
{
XRPL_ASSERT(
issuance.getType() == ltMPTOKEN_ISSUANCE,
"xrpl::setIssuerMirrorEpoch : issuance MPTokenIssuance object");
"xrpl::setIssuerMirrorCurrent : issuance MPTokenIssuance object");
XRPL_ASSERT(
mptoken.getType() == ltMPTOKEN, "xrpl::setIssuerMirrorEpoch : mptoken MPToken object");
mptoken.getType() == ltMPTOKEN, "xrpl::setIssuerMirrorCurrent : mptoken MPToken object");
// Unlike the auditor mirror, the issuer mirror is not optional: every
// confidential MPToken carries one, so there is no existence check here.
//
// Record the key and epoch under which the mirror is now encrypted. Both fields remain absent
// at epoch 0: such mirrors use the original issuer key. The original key is stored in
// sfInitialIssuerEncryptionKey after the first rotation.
if (auto const epoch = issuance[~sfIssuerKeyEpoch].value_or(0); epoch != 0)
{
mptoken[sfIssuerKeyMirrorEpoch] = epoch;
mptoken[sfIssuerMirrorEncryptionKey] = issuance[sfIssuerEncryptionKey];
}
}
void
@@ -466,9 +473,9 @@ setAuditorMirrorEpoch(SLE const& issuance, SLE& mptoken)
}
void
setMirrorEpochs(SLE const& issuance, SLE& mptoken)
setMirrorsCurrent(SLE const& issuance, SLE& mptoken)
{
setIssuerMirrorEpoch(issuance, mptoken);
setIssuerMirrorCurrent(issuance, mptoken);
setAuditorMirrorEpoch(issuance, mptoken);
}
+21
View File
@@ -1,10 +1,24 @@
#include <xrpl/protocol/MPTAmount.h>
#include <xrpl/basics/MathUtilities.h>
#include <xrpl/basics/contract.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Rules.h>
#include <stdexcept>
namespace xrpl {
MPTAmount&
MPTAmount::operator+=(MPTAmount const& other)
{
if (auto const result = checkedAdd(value_, other.value()))
{
value_ = *result;
return *this;
}
if (isFeatureEnabled(fixCleanup3_5_0, /*resultIfNoRules*/ true))
Throw<std::overflow_error>("MPTAmount::operator+= overflow");
value_ += other.value();
return *this;
}
@@ -12,6 +26,13 @@ MPTAmount::operator+=(MPTAmount const& other)
MPTAmount&
MPTAmount::operator-=(MPTAmount const& other)
{
if (auto const result = checkedSub(value_, other.value()))
{
value_ = *result;
return *this;
}
if (isFeatureEnabled(fixCleanup3_5_0, /*resultIfNoRules*/ true))
Throw<std::overflow_error>("MPTAmount::operator-= overflow");
value_ -= other.value();
return *this;
}
+38 -1
View File
@@ -3,10 +3,14 @@
#include <xrpl/beast/utility/Zero.h> // IWYU pragma: keep
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/protocol/Asset.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Rules.h>
#include <xrpl/protocol/STAmount.h>
#include <cstdint>
#include <limits> // IWYU pragma: keep
#include <optional>
#include <stdexcept>
namespace xrpl {
@@ -81,13 +85,46 @@ Quality::ceilInStrict(Amounts const& amount, STAmount const& limit, bool roundUp
return ceilInImpl<divRoundStrict>(amount, limit, roundUp, *this);
}
// Returns std::nullopt if a native result can't be represented, instead of
// throwing. Pathfinding runs without transaction
// rules, so default to the fixed behavior there to match transactions once the
// amendment is enabled.
template <STAmount (*MulRoundFunc)(STAmount const&, STAmount const&, Asset const&, bool)>
static std::optional<STAmount>
tryMulRound(STAmount const& v1, STAmount const& v2, Asset const& asset, bool roundUp)
{
try
{
return MulRoundFunc(v1, v2, asset, roundUp);
}
catch (std::overflow_error const&)
{
// std::overflow_error is a std::runtime_error, so it must be caught
// first to keep the handler below from swallowing it. It means the
// computation itself overflowed, not that the result is out of range.
// Callers handle it, e.g. BookStep removes the offer.
throw;
}
catch (std::runtime_error const&)
{
if (!asset.native() || !isFeatureEnabled(fixCleanup3_5_0, /*resultIfNoRules*/ true))
throw;
return std::nullopt;
}
}
template <STAmount (*MulRoundFunc)(STAmount const&, STAmount const&, Asset const&, bool)>
static Amounts
ceilOutImpl(Amounts const& amount, STAmount const& limit, bool roundUp, Quality const& quality)
{
if (amount.out > limit)
{
Amounts result(MulRoundFunc(limit, quality.rate(), amount.in.asset(), roundUp), limit);
// An unrepresentable XRP product is larger than amount.in, so the
// clamp below would return amount.in anyway.
Amounts result(
tryMulRound<MulRoundFunc>(limit, quality.rate(), amount.in.asset(), roundUp)
.value_or(amount.in),
limit);
// Clamp in
if (result.in > amount.in)
result.in = amount.in;
+85 -55
View File
@@ -1,6 +1,7 @@
#include <xrpl/protocol/STAmount.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/MathUtilities.h>
#include <xrpl/basics/Number.h>
#include <xrpl/basics/contract.h>
#include <xrpl/basics/safe_cast.h>
@@ -384,10 +385,11 @@ operator+(STAmount const& v1, STAmount const& v2)
return {v1.getFName(), v1.asset(), v2.mantissa(), v2.exponent(), v2.negative()};
}
// XRPAmount and MPTAmount check for int64 overflow under fixCleanup3_5_0.
if (v1.native())
return {v1.getFName(), getSNValue(v1) + getSNValue(v2)};
return {v1.getFName(), (v1.xrp() + v2.xrp()).drops()};
if (v1.holds<MPTIssue>())
return {v1.asset_, v1.mpt().value() + v2.mpt().value()};
return {v1.asset_, (v1.mpt() + v2.mpt()).value()};
auto x = v1;
x = v1.iou() + v2.iou();
@@ -490,16 +492,7 @@ canAdd(STAmount const& a, STAmount const& b)
// XRP case (overflow & underflow check)
if (isXRP(a) && isXRP(b))
{
XRPAmount const aVal = a.xrp();
XRPAmount const bVal = b.xrp();
return !(
(bVal > XRPAmount{0} &&
aVal > XRPAmount{std::numeric_limits<XRPAmount::value_type>::max()} - bVal) ||
(bVal < XRPAmount{0} &&
aVal < XRPAmount{std::numeric_limits<XRPAmount::value_type>::min()} - bVal));
}
return checkedAdd(a.xrp().drops(), b.xrp().drops()).has_value();
// IOU case (precision check)
auto const ret = std::visit(
@@ -516,15 +509,7 @@ canAdd(STAmount const& a, STAmount const& b)
// MPT (overflow & underflow check)
if constexpr (kIsMptissueV<TIss1> && kIsMptissueV<TIss2>)
{
MPTAmount const aVal = a.mpt();
MPTAmount const bVal = b.mpt();
return !(
(bVal > MPTAmount{0} &&
aVal > MPTAmount{std::numeric_limits<MPTAmount::value_type>::max()} - bVal) ||
(bVal < MPTAmount{0} &&
aVal < MPTAmount{std::numeric_limits<MPTAmount::value_type>::min()} - bVal));
}
return checkedAdd(a.mpt().value(), b.mpt().value()).has_value();
return std::nullopt;
},
a.asset().value(),
@@ -575,11 +560,7 @@ canSubtract(STAmount const& a, STAmount const& b)
return false;
// Check for overflow
if (bVal < XRPAmount{0} &&
aVal > XRPAmount{std::numeric_limits<XRPAmount::value_type>::max()} + bVal)
return false;
return true;
return checkedSub(aVal.drops(), bVal.drops()).has_value();
}
// IOU case (no underflow)
@@ -602,10 +583,7 @@ canSubtract(STAmount const& a, STAmount const& b)
return false;
// Overflow check
if (bVal < MPTAmount{0} &&
aVal > MPTAmount{std::numeric_limits<MPTAmount::value_type>::max()} + bVal)
return false;
return true;
return checkedSub(aVal.value(), bVal.value()).has_value();
}
return std::nullopt;
},
@@ -1247,6 +1225,19 @@ divide(STAmount const& num, STAmount const& den, Asset const& asset)
if (num == beast::kZero)
return {asset};
// The legacy path below assumes 16-digit mantissas. An MPT mantissa is
// the raw 63-bit value and is never scaled down, so an MPT numerator can
// overflow the quotient and an MPT denominator shortens it, moving the +5
// nudge into a significant digit. Use Number arithmetic under
// MPTokensV2 whenever an MPT is involved, as mulRound/divRound do. Pin
// the rounding mode so the result does not depend on the caller's.
if (isFeatureEnabled(featureMPTokensV2, /*resultIfNoRules*/ true) &&
(asset.holds<MPTIssue>() || num.holds<MPTIssue>() || den.holds<MPTIssue>()))
{
NumberRoundModeGuard const rg(Number::RoundingMode::ToNearest);
return STAmount{asset, Number{num} / Number{den}};
}
std::uint64_t numVal = num.mantissa();
std::uint64_t denVal = den.mantissa();
int numOffset = num.exponent();
@@ -1276,11 +1267,21 @@ divide(STAmount const& num, STAmount const& den, Asset const& asset)
// numerator by 10^17 (the product is in the range of
// 10^32 to 10^33) followed by a division, so the result
// is in the range of 10^16 to 10^15.
return STAmount(
asset,
muldiv(numVal, kTenTO17, denVal) + 5,
numOffset - denOffset - 17,
num.negative() != den.negative());
std::uint64_t quotient = muldiv(numVal, kTenTO17, denVal);
int offset = numOffset - denOffset - 17;
// An integral (XRP) numerator is not scaled down to 16 digits, so the
// quotient can land in [2^63, 2^64). The STAmount constructor reads
// that as a negative IOU mantissa, and the +5 can wrap. Drop a digit
// first; the result is canonicalized to 16 digits anyway.
if (quotient > static_cast<std::uint64_t>(std::numeric_limits<std::int64_t>::max()) - 5 &&
isFeatureEnabled(featureMPTokensV2, /*resultIfNoRules*/ true))
{
quotient /= 10;
++offset;
}
return STAmount(asset, quotient + 5, offset, num.negative() != den.negative());
}
STAmount
@@ -1486,28 +1487,28 @@ roundNumberResult(
// and for materializing the final integral amount.
NumberRoundModeGuard const finalRound(roundMode(resultNegative, roundUp));
auto result = STAmount{asset, number};
// An integral result can't underflow: when roundUp is set, roundMode()
// above selects Upward, and materializing a Number into an integral
// STAmount honors that mode (Number::operator rep()), so any positive
// value rounds up to at least 1.
[[maybe_unused]] bool const nonzeroPositiveRoundUp =
roundUp && !resultNegative && number != beast::kZero;
ALWAYS(
!nonzeroPositiveRoundUp || result != beast::kZero,
"xrpl::roundNumberResult : positive rounded-up MPT result is representable");
!nonzeroPositiveRoundUp || !asset.integral() || result != beast::kZero,
"xrpl::roundNumberResult : positive rounded-up integral result is representable");
if (roundUp && !resultNegative && !result)
{
// Intended to preserve existing mulRound/divRound behavior for a
// positive result too small to represent in the target asset.
//
// Unreachable in practice: when roundUp is set, roundMode() above
// selects Upward, and materializing a Number into an STAmount honors
// that mode (Number::operator rep()), so any positive value rounds up
// to at least the smallest representable unit. Hence, a positive result
// is never !result here; the only zero case is a zero operand, which
// the mulRound/divRound callers handle before reaching this function.
// LCOV_EXCL_START
// Preserve existing mulRound/divRound behavior for a positive result
// too small to represent in the target asset. An IOU result with an
// MPT operand (e.g. 1 MPT / 1e95) below the smallest IOU is flushed to
// zero by IOUAmount normalization regardless of the rounding mode, so
// snap it up to the smallest positive IOU. The only zero case for an
// integral result is a zero operand, which the mulRound/divRound
// callers handle before reaching this function.
if (asset.integral())
return STAmount{asset, 1};
return STAmount{asset, 1}; // LCOV_EXCL_LINE
return STAmount{asset, STAmount::kMinValue, STAmount::kMinOffset, false};
// LCOV_EXCL_STOP
}
return result;
@@ -1556,11 +1557,19 @@ mulRoundImpl(STAmount const& v1, STAmount const& v2, Asset const& asset, bool ro
bool const resultNegative = v1.negative() != v2.negative();
if (asset.holds<MPTIssue>() && isFeatureEnabled(featureMPTokensV2, false))
// The legacy path below assumes 16-digit mantissas. An MPT mantissa is the
// raw 63-bit balance, so any MPT operand (not just an MPT result) can
// overflow it: a large MPT amount times an IOU-shaped rate into an XRP or
// IOU result is the offer-crossing remainder and partial-fill case. Use
// Number arithmetic under MPTokensV2 whenever an MPT is involved.
//
// Outside a transaction (no current rules, e.g. pathfinding) default to
// the Number path: an MPT operand can only reach here through MPT
// offers or AMMs, which require MPTokensV2, so this keeps RPC quotes on
// the same arithmetic as the transaction they describe.
if (isFeatureEnabled(featureMPTokensV2, /*resultIfNoRules*/ true) &&
(asset.holds<MPTIssue>() || v1.holds<MPTIssue>() || v2.holds<MPTIssue>()))
{
// MPT DEX can combine 63-bit MPT amounts with IOU-shaped transfer
// rates. Use Number arithmetic under MPTokensV2 so the rounded
// operation is not limited by the legacy uint64_t scaled mantissa.
Number result;
{
NumberRoundModeGuard const operationRound(roundMode(resultNegative, roundUp));
@@ -1658,9 +1667,12 @@ divRoundImpl(STAmount const& num, STAmount const& den, Asset const& asset, bool
bool const resultNegative = (num.negative() != den.negative());
// See mulRoundImpl: any MPT operand, not just an MPT result, and the
// Number path when there are no current rules.
// fixCleanup3_5_0: the legacy path below overflows on large MPT amounts.
if (asset.holds<MPTIssue>() &&
(isFeatureEnabled(featureMPTokensV2, true) || isFeatureEnabled(fixCleanup3_5_0, true)))
auto const enabled = isFeatureEnabled(featureMPTokensV2, /*resultIfNoRules*/ true) ||
isFeatureEnabled(fixCleanup3_5_0, /*resultIfNoRules*/ true);
if ((asset.holds<MPTIssue>() || num.holds<MPTIssue>() || den.holds<MPTIssue>()) && enabled)
{
// Match the multiply path above: Number performs the rounded
// operation, then STAmount materializes the final MPT amount using the
@@ -1751,6 +1763,24 @@ divRoundStrict(STAmount const& num, STAmount const& den, Asset const& asset, boo
return divRoundImpl<NumberRoundModeGuard>(num, den, asset, roundUp);
}
STAmount
mulRatio(STAmount const& amt, std::uint32_t num, std::uint32_t den, Number::RoundingMode rounding)
{
XRPL_ASSERT(
rounding == Number::RoundingMode::Upward || rounding == Number::RoundingMode::Downward,
"xrpl::mulRatio(STAmount) : rounding is Upward or Downward");
bool const roundUp = rounding == Number::RoundingMode::Upward;
return amt.asset().visit(
[&](Issue const& issue) -> STAmount {
if (amt.native())
return mulRatio(amt.xrp(), num, den, roundUp);
return {mulRatio(amt.iou(), num, den, roundUp), issue};
},
[&](MPTIssue const& issue) -> STAmount {
return {mulRatio(amt.mpt(), num, den, roundUp), issue};
});
}
[[nodiscard]] bool
STAmount::isZeroAtScale(int scale) const
{
+1
View File
@@ -228,6 +228,7 @@ transResults()
MAKE_ERROR(terNO_DELEGATE_PERMISSION, "Delegated account lacks permission to perform this transaction."),
MAKE_ERROR(terLOCKED, "Fund is locked."),
MAKE_ERROR(terNO_PERMISSION, "No permission to perform requested operation."),
MAKE_ERROR(terTICKET_RESERVED, "Ticket is reserved."),
MAKE_ERROR(tesSUCCESS, "The transaction was applied. Only final in a validated ledger."),
};
+54
View File
@@ -0,0 +1,54 @@
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/basics/MathUtilities.h>
#include <xrpl/basics/contract.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Rules.h>
#include <stdexcept>
namespace xrpl {
XRPAmount&
XRPAmount::operator+=(value_type const& rhs)
{
if (auto const result = checkedAdd(drops_, rhs))
{
drops_ = *result;
return *this;
}
if (isFeatureEnabled(fixCleanup3_5_0, /*resultIfNoRules*/ true))
Throw<std::overflow_error>("XRPAmount::operator+= overflow");
drops_ += rhs;
return *this;
}
XRPAmount&
XRPAmount::operator-=(value_type const& rhs)
{
if (auto const result = checkedSub(drops_, rhs))
{
drops_ = *result;
return *this;
}
if (isFeatureEnabled(fixCleanup3_5_0, /*resultIfNoRules*/ true))
Throw<std::overflow_error>("XRPAmount::operator-= overflow");
drops_ -= rhs;
return *this;
}
XRPAmount&
XRPAmount::operator*=(value_type const& rhs)
{
if (auto const result = checkedMul(drops_, rhs))
{
drops_ = *result;
return *this;
}
if (isFeatureEnabled(fixCleanup3_5_0, /*resultIfNoRules*/ true))
Throw<std::overflow_error>("XRPAmount::operator*= overflow");
drops_ *= rhs;
return *this;
}
} // namespace xrpl
+211 -77
View File
@@ -26,6 +26,7 @@
#include <boost/smart_ptr/intrusive_ptr.hpp>
#include <atomic>
#include <cstdint>
#include <exception>
#include <functional>
@@ -77,14 +78,23 @@ SHAMap::SHAMap(SHAMap const& other, bool isMutable)
: f_(other.f_)
, journal_(other.f_.journal())
, cowid_(other.cowid_ + 1)
, ledgerSeq_(other.ledgerSeq_)
, ledgerSeq_(other.ledgerSeq())
, root_(other.root_)
, state_(isMutable ? SHAMapState::Modifying : SHAMapState::Immutable)
, type_(other.type_)
, backed_(other.backed_)
{
// A snapshot shares the source's root, so Invalid carries over. Read once into a local, since
// the source's state can change while this runs.
auto const otherState = other.state();
auto const ownState = [&] {
if (otherState == SHAMapState::Invalid)
return SHAMapState::Invalid;
return isMutable ? SHAMapState::Modifying : SHAMapState::Immutable;
}();
state_.store(ownState, std::memory_order_release);
// If either map may change, they cannot share nodes
if ((state_ != SHAMapState::Immutable) || (other.state_ != SHAMapState::Immutable))
if ((ownState != SHAMapState::Immutable) || (otherState != SHAMapState::Immutable))
{
unshare();
}
@@ -96,6 +106,18 @@ SHAMap::snapShot(bool isMutable) const
return std::make_shared<SHAMap>(*this, isMutable);
}
void
SHAMap::condemn(SHAMapTreeNode const& node, SHAMapHash const& hash, SHAMapNodeID const& position)
const
{
// journal_ is shared by every map built from one Family, so name which map this is. The root
// node's own hash, since getHash() unshares the tree on a zero hash.
JLOG(journal_.warn()) << (node.isLeaf() ? "Leaf " : "Inner node ") << hash << " cannot sit at "
<< position << ", so the map with root hash " << root_->getHash()
<< " is invalid";
setInvalid();
}
void
SHAMap::dirtyUp(NodePathStack& stack, UInt256 const& target, SHAMapTreeNodePtr child)
{
@@ -105,20 +127,25 @@ SHAMap::dirtyUp(NodePathStack& stack, UInt256 const& target, SHAMapTreeNodePtr c
// child can be an inner node or a leaf
XRPL_ASSERT(
(state_ != SHAMapState::Synching) && (state_ != SHAMapState::Immutable),
(state() != SHAMapState::Synching) && (state() != SHAMapState::Immutable),
"xrpl::SHAMap::dirtyUp : valid state");
XRPL_ASSERT(child && (child->cowid() == cowid_), "xrpl::SHAMap::dirtyUp : valid child input");
while (!stack.empty())
{
auto node = intr_ptr::dynamicPointerCast<SHAMapInnerNode>(stack.top().first);
SHAMapNodeID const nodeID = stack.top().second;
stack.pop();
XRPL_ASSERT(node, "xrpl::SHAMap::dirtyUp : non-null node");
auto const depth = stack.topDepth();
auto node = stack.releaseNodeAs<SHAMapInnerNode>();
if (!node)
{
// LCOV_EXCL_START
UNREACHABLE("xrpl::SHAMap::dirtyUp : node is not inner");
Throw<SHAMapMissingNode>(type_, target);
// LCOV_EXCL_STOP
}
auto const branch = selectBranch(nodeID, target);
auto const branch = selectBranch(depth, target);
node = unshareNode(std::move(node), nodeID);
node = unshareNode(std::move(node), depth);
node->setChild(branch, std::move(child));
child = std::move(node);
@@ -128,32 +155,62 @@ SHAMap::dirtyUp(NodePathStack& stack, UInt256 const& target, SHAMapTreeNodePtr c
SHAMapLeafNode*
SHAMap::walkTowardsKey(UInt256 const& id, NodePathStack* stack) const
{
XRPL_ASSERT(
stack == nullptr || stack->empty(), "xrpl::SHAMap::walkTowardsKey : empty stack input");
auto inNode = root_;
SHAMapNodeID nodeID;
if (stack != nullptr && !stack->empty())
{
// The path must start at the root, so a non-empty one is cleared rather than appended to.
// LCOV_EXCL_START
UNREACHABLE("xrpl::SHAMap::walkTowardsKey : non-empty stack input");
stack->clear();
return nullptr;
// LCOV_EXCL_STOP
}
// Every node on this walk lies on the path to `id`, so the stack can derive each ID from the
// branch `id` selects at the node above it.
auto const pushCurrent = [&] {
if (stack != nullptr)
stack->pushNode(inNode, id);
auto inNode = root_;
unsigned int noStackDepth = 0;
// Returns false when the path could not be extended, which is separate from `id` being absent
// (see pushChild). The path is cleared, so a caller reads an empty path as a refusal.
auto const pushCurrent = [&] -> bool {
if (stack == nullptr || stack->pushNode(inNode, id))
{
return true;
}
stack->clear();
return false;
};
while (inNode->isInner())
{
pushCurrent();
if (!pushCurrent())
{
return nullptr;
}
auto& inner = safeDowncast<SHAMapInnerNode&>(*inNode);
auto const branch = selectBranch(nodeID, id);
auto const depth = stack != nullptr ? stack->topDepth() : noStackDepth;
auto const branch = selectBranch(depth, id);
if (inner.isEmptyBranch(branch))
return nullptr;
inNode = descendThrow(inner, branch);
nodeID = nodeID.getChildNodeID(branch);
if (stack == nullptr)
{
// The same depth bound pushChild applies, so both modes refuse at the same depth. A
// node resolved from the local store still has its position and type to be judged.
bool const tooDeep = pastLeafDepth(depth, *inNode);
SOMETIMES(tooDeep, "xrpl::SHAMap::walkTowardsKey : child too deep");
if (tooDeep)
{
return nullptr;
}
++noStackDepth;
}
}
pushCurrent();
if (!pushCurrent())
{
return nullptr;
}
return safeDowncast<SHAMapLeafNode*>(inNode.get());
}
@@ -170,7 +227,7 @@ SHAMapTreeNodePtr
SHAMap::fetchNodeFromDB(SHAMapHash const& hash) const
{
XRPL_ASSERT(backed_, "xrpl::SHAMap::fetchNodeFromDB : is backed");
auto const obj = f_.db().fetchNodeObject(hash.asUInt256(), ledgerSeq_);
auto const obj = f_.db().fetchNodeObject(hash.asUInt256(), ledgerSeq());
return finishFetch(hash, obj);
}
@@ -183,10 +240,17 @@ SHAMap::finishFetch(SHAMapHash const& hash, std::shared_ptr<NodeObject> const& o
{
if (!object)
{
if (full_)
// A missing node disproves full_, so withdraw it and report the gap. The exchange
// makes exactly one reader report it, and its acquire pairs with the release store in
// setFull(). It needs no release: the only other reader of full_ is this same path,
// and a reader that loses the exchange does nothing with what the winner did. The load
// ahead of it is relaxed, since all it decides is whether to try the exchange: a stale
// true costs one exchange that loses, and a stale false means another reader won, or
// setFull() is not visible yet and the gap goes unreported.
if (full_.load(std::memory_order_relaxed) &&
full_.exchange(false, std::memory_order_acquire))
{
full_ = false;
f_.missingNodeAcquireBySeq(ledgerSeq_, hash.asUInt256());
f_.missingNodeAcquireBySeq(ledgerSeq(), hash.asUInt256());
}
return {};
}
@@ -219,7 +283,7 @@ SHAMap::checkFilter(SHAMapHash const& hash, SHAMapSyncFilter const* filter) cons
auto node = SHAMapTreeNode::makeFromPrefix(makeSlice(*nodeData), hash);
if (node)
{
filter->gotNode(true, hash, ledgerSeq_, std::move(*nodeData), node->getType());
filter->gotNode(true, hash, ledgerSeq(), std::move(*nodeData), node->getType());
if (backed_)
canonicalize(hash, node);
}
@@ -357,12 +421,23 @@ SHAMap::descend(
!parent->isEmptyBranch(branch), "xrpl::SHAMap::descend : parent branch is non-empty");
SHAMapTreeNode* child = parent->getChildPointer(branch); // NOLINT(misc-const-correctness)
auto const childID = parentID.getChildNodeID(branch);
if (child == nullptr)
{
auto const& childHash = parent->getChildHash(branch);
SHAMapTreeNodePtr childNode = fetchNodeNT(childHash, filter);
if (childNode &&
(!belongsAt(childID, *childNode) || pastLeafDepth(parentID.getDepth(), *childNode)))
{
// A hash covers a node's contents, not its position, so position is judged before
// canonicalizeChild hooks the node in. The verdict lands on the map, since resolving
// the same blob again gives the same node.
condemn(*childNode, childHash, childID);
return std::make_pair(nullptr, childID);
}
if (childNode)
{
childNode = parent->canonicalizeChild(branch, std::move(childNode));
@@ -370,7 +445,7 @@ SHAMap::descend(
}
}
return std::make_pair(child, parentID.getChildNodeID(branch));
return std::make_pair(child, childID);
}
SHAMapTreeNode*
@@ -399,7 +474,7 @@ SHAMap::descendAsync(
{
f_.db().asyncFetch(
hash.asUInt256(),
ledgerSeq_,
ledgerSeq(),
[this, hash, cb{std::move(callback)}](std::shared_ptr<NodeObject> const& object) {
auto const node = finishFetch(hash, object);
cb(node, hash);
@@ -417,16 +492,16 @@ SHAMap::descendAsync(
template <class Node>
intr_ptr::SharedPtr<Node>
SHAMap::unshareNode(intr_ptr::SharedPtr<Node> node, SHAMapNodeID const& nodeID)
SHAMap::unshareNode(intr_ptr::SharedPtr<Node> node, unsigned int depth)
{
// make sure the node is suitable for the intended operation (copy on write)
XRPL_ASSERT(node->cowid() <= cowid_, "xrpl::SHAMap::unshareNode : node valid for cowid");
if (node->cowid() != cowid_)
{
// have a CoW
XRPL_ASSERT(state_ != SHAMapState::Immutable, "xrpl::SHAMap::unshareNode : not immutable");
XRPL_ASSERT(state() != SHAMapState::Immutable, "xrpl::SHAMap::unshareNode : not immutable");
node = intr_ptr::staticPointerCast<Node>(node->clone(cowid_));
if (nodeID.isRoot())
if (depth == 0)
root_ = node;
}
return node;
@@ -436,14 +511,19 @@ SHAMapLeafNode*
SHAMap::belowHelper(NodePathStack& stack, BelowDirection direction) const
{
XRPL_ASSERT(!stack.empty(), "xrpl::SHAMap::belowHelper : non-empty stack input");
if (auto const& top = stack.top().first; top->isLeaf())
if (stack.empty())
{
// LCOV_EXCL_START
return nullptr;
// LCOV_EXCL_STOP
}
if (auto const& top = stack.top(); top->isLeaf())
return safeDowncast<SHAMapLeafNode*>(top.get());
// The stack owns the node/ID pairing, so descending is only ever "push the branch we took".
// `scanned` counts how many branches of the current node we have examined; the branch we look
// at is derived from it, so no index ever goes out of range. `inner` tracks the node on top of
// the stack, which keeps it alive, so it only needs recomputing after a push.
auto* inner = safeDowncast<SHAMapInnerNode*>(stack.top().first.get());
// `scanned` counts the branches of the current node already examined, and the branch looked at
// is derived from it, so the index stays inside the fan-out. `inner` points at the top of the
// path, which keeps it alive, so it only needs recomputing after a push.
auto* inner = safeDowncast<SHAMapInnerNode*>(stack.top().get());
for (auto scanned = 0u; scanned < kBranchFactor;)
{
auto const childBranch =
@@ -455,9 +535,20 @@ SHAMap::belowHelper(NodePathStack& stack, BelowDirection direction) const
continue;
}
stack.pushChild(descendThrow(*inner, childBranch), childBranch);
auto const parentDepth = stack.topDepth();
auto descended = descendThrow(*inner, childBranch);
if (!stack.pushChild(std::move(descended), childBranch))
{
// Throwing rather than returning nullptr keeps nullptr meaning a subtree with no leaf
// below it, which begin() and an iterator increment both rely on. Every caller here is
// a const read on a shared snapshot, so the verdict is left to the acquisition path
// (see SHAMap::descend and gmnProcessNodes).
JLOG(journal_.warn()) << "Cannot walk below depth " << parentDepth << " at branch "
<< childBranch;
Throw<SHAMapMissingNode>(type_, inner->getChildHash(childBranch));
}
auto const& child = stack.top().first;
auto const& child = stack.top();
if (child->isLeaf())
return safeDowncast<SHAMapLeafNode*>(child.get());
@@ -512,7 +603,12 @@ SHAMapLeafNode const*
SHAMap::peekFirstItem(NodePathStack& stack) const
{
XRPL_ASSERT(stack.empty(), "xrpl::SHAMap::peekFirstItem : empty stack input");
stack.pushRoot(root_);
if (!stack.pushRoot(root_))
{
// LCOV_EXCL_START
return nullptr;
// LCOV_EXCL_STOP
}
SHAMapLeafNode const* node = belowHelper(stack, BelowDirection::First);
if (node == nullptr)
{
@@ -526,18 +622,28 @@ SHAMapLeafNode const*
SHAMap::peekNextItem(UInt256 const& id, NodePathStack& stack) const
{
XRPL_ASSERT(!stack.empty(), "xrpl::SHAMap::peekNextItem : non-empty stack input");
XRPL_ASSERT(stack.top().first->isLeaf(), "xrpl::SHAMap::peekNextItem : stack starts with leaf");
if (stack.empty())
{
// LCOV_EXCL_START
return nullptr;
// LCOV_EXCL_STOP
}
XRPL_ASSERT(stack.top()->isLeaf(), "xrpl::SHAMap::peekNextItem : stack starts with leaf");
stack.pop();
while (!stack.empty())
{
auto const [node, nodeID] = stack.top();
auto const& node = stack.top();
XRPL_ASSERT(!node->isLeaf(), "xrpl::SHAMap::peekNextItem : another node is not leaf");
auto& inner = safeDowncast<SHAMapInnerNode&>(*node);
for (auto i = selectBranch(nodeID, id) + 1; i < kBranchFactor; ++i)
for (auto i = selectBranch(stack.topDepth(), id) + 1; i < kBranchFactor; ++i)
{
if (!inner.isEmptyBranch(i))
{
stack.pushChild(descendThrow(inner, i), i);
auto child = descendThrow(inner, i);
if (!stack.pushChild(std::move(child), i))
{
Throw<SHAMapMissingNode>(type_, id);
}
auto leaf = belowHelper(stack, BelowDirection::First);
if (leaf == nullptr)
Throw<SHAMapMissingNode>(type_, id);
@@ -581,9 +687,15 @@ SHAMap::boundHelper(UInt256 const& id, BelowDirection direction) const
NodePathStack stack;
walkTowardsKey(id, &stack);
// An empty path means the walk refused a node. An empty map still leaves its root on the path,
// and end() would claim no key lies on the requested side of `id`, so a refusal throws.
if (stack.empty())
Throw<SHAMapMissingNode>(type_, id);
while (!stack.empty())
{
auto const [node, nodeID] = stack.top();
auto const& node = stack.top();
if (node->isLeaf())
{
auto const& item = safeDowncast<SHAMapLeafNode const&>(*node).peekItem();
@@ -593,7 +705,7 @@ SHAMap::boundHelper(UInt256 const& id, BelowDirection direction) const
else
{
auto& inner = safeDowncast<SHAMapInnerNode&>(*node);
auto const taken = selectBranch(nodeID, id);
auto const taken = selectBranch(stack.topDepth(), id);
auto const remaining = searchingForward ? (kBranchFactor - 1u - taken) : taken;
for (auto scanned = 0u; scanned < remaining; ++scanned)
@@ -603,7 +715,11 @@ SHAMap::boundHelper(UInt256 const& id, BelowDirection direction) const
if (inner.isEmptyBranch(branch))
continue;
stack.pushChild(descendThrow(inner, branch), branch);
auto child = descendThrow(inner, branch);
if (!stack.pushChild(std::move(child), branch))
{
Throw<SHAMapMissingNode>(type_, id);
}
auto const leaf = belowHelper(stack, direction);
if (leaf == nullptr)
Throw<SHAMapMissingNode>(type_, id);
@@ -637,7 +753,7 @@ bool
SHAMap::delItem(UInt256 const& id)
{
// delete the item with this ID
XRPL_ASSERT(state_ != SHAMapState::Immutable, "xrpl::SHAMap::delItem : not immutable");
XRPL_ASSERT(state() != SHAMapState::Immutable, "xrpl::SHAMap::delItem : not immutable");
NodePathStack stack;
walkTowardsKey(id, &stack);
@@ -645,10 +761,12 @@ SHAMap::delItem(UInt256 const& id)
if (stack.empty())
Throw<SHAMapMissingNode>(type_, id);
auto const leaf = intr_ptr::dynamicPointerCast<SHAMapLeafNode>(stack.top().first);
stack.pop();
// An absent id leaves an inner node on top, which is the "not found" answer.
auto const leaf = stack.releaseNodeAs<SHAMapLeafNode>();
if (!leaf)
return false;
if (!leaf || (leaf->peekItem()->key() != id))
if (leaf->peekItem()->key() != id)
return false;
SHAMapNodeType const type = leaf->getType();
@@ -658,19 +776,25 @@ SHAMap::delItem(UInt256 const& id)
while (!stack.empty())
{
auto node = intr_ptr::staticPointerCast<SHAMapInnerNode>(stack.top().first);
SHAMapNodeID const nodeID = stack.top().second;
stack.pop();
auto const depth = stack.topDepth();
auto node = stack.releaseNodeAs<SHAMapInnerNode>();
if (!node)
{
// LCOV_EXCL_START
UNREACHABLE("xrpl::SHAMap::delItem : node is not inner");
Throw<SHAMapMissingNode>(type_, id);
// LCOV_EXCL_STOP
}
node = unshareNode(std::move(node), nodeID);
node = unshareNode(std::move(node), depth);
node->setChild(
selectBranch(nodeID, id), std::move(prevNode)); // NOLINT(bugprone-use-after-move)
selectBranch(depth, id), std::move(prevNode)); // NOLINT(bugprone-use-after-move)
XRPL_ASSERT(
not prevNode, // NOLINT(bugprone-use-after-move)
"xrpl::SHAMap::delItem : prevNode should be nullptr after std::move");
if (!nodeID.isRoot())
if (depth != 0)
{
// we may have made this a node with 1 or 0 children
// And, if so, we need to remove this branch
@@ -719,7 +843,7 @@ SHAMap::delItem(UInt256 const& id)
bool
SHAMap::addGiveItem(SHAMapNodeType type, boost::intrusive_ptr<SHAMapItem const> item)
{
XRPL_ASSERT(state_ != SHAMapState::Immutable, "xrpl::SHAMap::addGiveItem : not immutable");
XRPL_ASSERT(state() != SHAMapState::Immutable, "xrpl::SHAMap::addGiveItem : not immutable");
XRPL_ASSERT(type != SHAMapNodeType::TnInner, "xrpl::SHAMap::addGiveItem : valid type input");
// add the specified item, does not update
@@ -731,8 +855,8 @@ SHAMap::addGiveItem(SHAMapNodeType type, boost::intrusive_ptr<SHAMapItem const>
if (stack.empty())
Throw<SHAMapMissingNode>(type_, tag);
auto [node, nodeID] = stack.top();
stack.pop();
auto depth = stack.topDepth();
auto node = stack.releaseNode();
if (node->isLeaf())
{
@@ -740,12 +864,12 @@ SHAMap::addGiveItem(SHAMapNodeType type, boost::intrusive_ptr<SHAMapItem const>
if (leaf->peekItem()->key() == tag)
return false;
}
node = unshareNode(std::move(node), nodeID);
node = unshareNode(std::move(node), depth);
if (node->isInner())
{
// easy case, we end on an inner node
auto const inner = intr_ptr::staticPointerCast<SHAMapInnerNode>(node);
auto const branch = selectBranch(nodeID, tag);
auto const branch = selectBranch(depth, tag);
XRPL_ASSERT(
inner->isEmptyBranch(branch), "xrpl::SHAMap::addGiveItem : inner branch is empty");
inner->setChild(branch, makeTypedLeaf(type, std::move(item), cowid_));
@@ -763,13 +887,20 @@ SHAMap::addGiveItem(SHAMapNodeType type, boost::intrusive_ptr<SHAMapItem const>
auto b1 = 0u, b2 = 0u;
while ((b1 = selectBranch(nodeID, tag)) == (b2 = selectBranch(nodeID, otherItem->key())))
while ((b1 = selectBranch(depth, tag)) == (b2 = selectBranch(depth, otherItem->key())))
{
stack.pushNode(node, tag);
if (!stack.pushNode(node, tag))
{
// The loop advances only while the two keys agree at the current nibble, and keys
// agreeing at every nibble are equal, which returned false above.
// LCOV_EXCL_START
Throw<SHAMapMissingNode>(type_, tag);
// LCOV_EXCL_STOP
}
// we need a new inner node, since both go on same branch at this
// level
nodeID = nodeID.getChildNodeID(b1);
++depth;
node = intr_ptr::makeShared<SHAMapInnerNode>(cowid_);
}
@@ -810,7 +941,7 @@ SHAMap::updateGiveItem(SHAMapNodeType type, boost::intrusive_ptr<SHAMapItem cons
// can't change the tag but can change the hash
UInt256 const tag = item->key();
XRPL_ASSERT(state_ != SHAMapState::Immutable, "xrpl::SHAMap::updateGiveItem : not immutable");
XRPL_ASSERT(state() != SHAMapState::Immutable, "xrpl::SHAMap::updateGiveItem : not immutable");
NodePathStack stack;
walkTowardsKey(tag, &stack);
@@ -818,16 +949,19 @@ SHAMap::updateGiveItem(SHAMapNodeType type, boost::intrusive_ptr<SHAMapItem cons
if (stack.empty())
Throw<SHAMapMissingNode>(type_, tag);
auto node = intr_ptr::dynamicPointerCast<SHAMapLeafNode>(stack.top().first);
auto const nodeID = stack.top().second;
stack.pop();
auto const depth = stack.topDepth();
if (!node || (node->peekItem()->key() != tag))
// A tag absent from the map leaves an inner node on top, which the API permits.
auto node = stack.releaseNodeAs<SHAMapLeafNode>();
if (!node)
{
return false;
}
// Or the walk ended on a leaf holding some other key.
if (node->peekItem()->key() != tag)
{
// LCOV_EXCL_START
UNREACHABLE("xrpl::SHAMap::updateGiveItem : invalid node");
return false;
// LCOV_EXCL_STOP
}
if (node->getType() != type)
@@ -836,7 +970,7 @@ SHAMap::updateGiveItem(SHAMapNodeType type, boost::intrusive_ptr<SHAMapItem cons
return false;
}
node = unshareNode(std::move(node), nodeID);
node = unshareNode(std::move(node), depth);
if (node->setItem(item))
dirtyUp(stack, tag, node);
@@ -901,7 +1035,7 @@ SHAMap::writeNode(NodeObjectType t, SHAMapTreeNodePtr node) const
Serializer s;
node->serializeWithPrefix(s);
f_.db().store(t, std::move(s.modData()), node->getHash().asUInt256(), ledgerSeq_);
f_.db().store(t, std::move(s.modData()), node->getHash().asUInt256(), ledgerSeq());
return node;
}
+4 -1
View File
@@ -16,6 +16,7 @@
#include <xrpl/shamap/detail/TaggedPointer.h>
#include <xrpl/shamap/detail/TaggedPointer.ipp>
#include <atomic>
#include <cstddef>
#include <cstdint>
#include <mutex>
@@ -77,7 +78,9 @@ SHAMapInnerNode::clone(std::uint32_t cowid) const
auto p = intr_ptr::makeShared<SHAMapInnerNode>(cowid, branchCount);
p->hash_ = hash_;
p->isBranch_ = isBranch_;
p->fullBelowGen_ = fullBelowGen_;
// Relaxed, as everywhere. p is not reachable by another thread until this returns.
p->fullBelowGen_.store(
fullBelowGen_.load(std::memory_order_relaxed), std::memory_order_relaxed);
SHAMapHash* cloneHashes = nullptr;
SHAMapHash* thisHashes = nullptr;
SHAMapTreeNodePtr* cloneChildren = nullptr;
+53 -13
View File
@@ -50,14 +50,31 @@ maskedToDepth(UInt256 const& key, unsigned int depth)
return key & depthMask(depth);
}
// Whether `id` at `depth` is what `key` looks like once masked down to that depth, i.e.
// whether an ID with this depth and id names a subtree that `key` falls under.
// Whether `key` masked to `depth` equals `id`. Asymmetric: `id` is not masked, so this also
// refuses an id carrying bits below its own depth.
static bool
isPrefixOfAtDepth(UInt256 const& id, unsigned int depth, UInt256 const& key)
{
return maskedToDepth(key, depth) == id;
}
bool
samePositionAtDepth(unsigned int depth, UInt256 const& lhs, UInt256 const& rhs)
{
// The mask is chosen here, so an out-of-range depth would index depthMask's table past its
// last entry. kLeafDepth itself is in range, unlike in selectBranch: the position at the leaf
// depth is the whole key. A public helper has to hold its own bound.
if (depth > SHAMap::kLeafDepth)
{
// LCOV_EXCL_START
UNREACHABLE("xrpl::samePositionAtDepth : depth within tree");
depth = SHAMap::kLeafDepth;
// LCOV_EXCL_STOP
}
return maskedToDepth(lhs, depth) == maskedToDepth(rhs, depth);
}
// canonicalize the hash to a node ID for this depth
SHAMapNodeID::SHAMapNodeID(unsigned int depth, UInt256 const& hash) : id_(hash), depth_(depth)
{
@@ -90,6 +107,31 @@ SHAMapNodeID::getRawString() const
return s.getString();
}
[[nodiscard]] UInt256
childNodeID(UInt256 const& parentID, unsigned int parentDepth, unsigned int branch)
{
XRPL_ASSERT(branch < SHAMap::kBranchFactor, "xrpl::childNodeID : valid branch input");
XRPL_ASSERT(parentDepth < SHAMap::kLeafDepth, "xrpl::childNodeID : parent above leaf depth");
// Only a depth below kLeafDepth names a child nibble, so clamp to the last one that does.
auto const clamped = std::min(parentDepth, SHAMap::kLeafDepth - 1u);
// The nibble is cleared before the branch is written, so an id already carrying one there is
// overwritten rather than merged. The byte index and both nibble choices read `clamped`, so
// they cannot disagree.
UInt256 id = parentID;
auto& byte = *(id.begin() + (clamped / 2));
if ((clamped & 1) != 0u)
{
byte = static_cast<unsigned char>((byte & 0xF0u) | branch);
}
else
{
byte = static_cast<unsigned char>((byte & 0x0Fu) | (branch << 4));
}
return id;
}
SHAMapNodeID
SHAMapNodeID::getChildNodeID(unsigned int branch) const
{
@@ -107,15 +149,13 @@ SHAMapNodeID::getChildNodeID(unsigned int branch) const
XRPL_ASSERT(
depth_ <= SHAMap::kLeafDepth, "xrpl::SHAMapNodeID::getChildNodeID : maximum leaf depth");
if (depth_ >= SHAMap::kLeafDepth)
if (SHAMap::isLeafDepth(depth_))
Throw<std::logic_error>(std::format("Request for child node ID of {}", to_string(*this)));
if (!isPrefixOf(id_))
Throw<std::logic_error>(std::format("Incorrect mask for {}", to_string(*this)));
SHAMapNodeID node{depth_ + 1, id_};
node.id_.begin()[depth_ / 2] |= ((depth_ & 1) != 0u) ? branch : (branch << 4);
return node;
return SHAMapNodeID{depth_ + 1, childNodeID(id_, depth_, branch)};
}
bool
@@ -145,16 +185,16 @@ deserializeSHAMapNodeID(void const* data, std::size_t size)
}
[[nodiscard]] unsigned int
selectBranch(SHAMapNodeID const& id, UInt256 const& hash)
selectBranch(unsigned int depth, UInt256 const& hash)
{
XRPL_ASSERT(id.getDepth() < SHAMap::kLeafDepth, "xrpl::selectBranch : depth below leaf depth");
XRPL_ASSERT(depth < SHAMap::kLeafDepth, "xrpl::selectBranch : depth below leaf depth");
// A depth-64 ID has no nibble left to select. Callers must not ask, but clamp anyway to keep
// the read below the end of the 32-byte key.
auto const depth = std::min(id.getDepth(), SHAMap::kLeafDepth - 1u);
auto branch = static_cast<unsigned int>(*(hash.begin() + (depth / 2)));
// Only a depth below kLeafDepth has a nibble to select, so clamp to the last one that does.
auto const clamped = std::min(depth, SHAMap::kLeafDepth - 1u);
auto branch = static_cast<unsigned int>(*(hash.begin() + (clamped / 2)));
if ((depth & 1) != 0u)
// The byte index and the nibble choice both read `clamped`, so they cannot disagree.
if ((clamped & 1) != 0u)
{
branch &= 0xf;
}
+146 -58
View File
@@ -143,13 +143,10 @@ SHAMap::visitDifferences(
if (!function(*node))
return;
// Nibbles run out at kLeafDepth, so only a leaf belongs there. A well-formed map never
// holds an inner node at that depth: addKnownNode marks the map invalid rather than hooking
// one in, and fetch-pack data is hash-verified against a validated root, so reaching this
// means a defect or a corrupt store, not something a peer can provoke. Report the node
// anyway - the wire form carries no depth, and the recipient hooks blobs in by hash - but
// skip the children rather than letting getChildNodeID throw on them.
if (nodeID.getDepth() >= kLeafDepth)
// Only a leaf belongs at kLeafDepth. The node is still reported, since the wire form
// carries no depth and the recipient hooks blobs by hash. Its children are skipped, since
// getChildNodeID has no answer past kLeafDepth.
if (isLeafDepth(nodeID.getDepth()))
{
// LCOV_EXCL_START
UNREACHABLE("xrpl::SHAMap::visitDifferences : inner node at leaf depth");
@@ -207,7 +204,12 @@ SHAMap::gmnProcessNodes(MissingNodes& mn, MissingNodes::StackEntry& se)
// we already know this child node is missing
fullBelow = false;
}
else if (!backed_ || !f_.getFullBelowCache()->touchIfExists(childHash.asUInt256()))
// The cache key carries the child's position beside its hash, so a hit answers for this
// child at this position and no other. The depth test runs first, so a child at kLeafDepth
// is judged by the arm below rather than answered from the cache: only a leaf sits there.
else if (
!backed_ || isLeafDepth(nodeID.getDepth() + 1) ||
!f_.getFullBelowCache()->touchIfExists(childHash.asUInt256(), nodeID, branch))
{
bool pending = false;
auto d = descendAsync(
@@ -238,7 +240,28 @@ SHAMap::gmnProcessNodes(MissingNodes& mn, MissingNodes::StackEntry& se)
if (--mn.max <= 0)
return;
}
else if (d->isInner() && !safeDowncast<SHAMapInnerNode*>(d)->isFullBelow(mn.generation))
// Only a leaf has a position of its own to judge, so the type is tested first. The
// depth test bounds the branch this arm names, and the arm below bounds the descent.
else if (
d->isLeaf() && !isLeafDepth(nodeID.getDepth()) && !belongsAt(nodeID, branch, *d))
{
// The judgment SHAMap::descend makes, for the descendAsync path, which hooks what
// it resolves before this runs. `fullBelow` is cleared as on the missing path.
fullBelow = false;
condemn(*d, childHash, nodeID.getChildNodeID(branch));
return;
}
else if (pastLeafDepth(nodeID.getDepth(), *d))
{
// A node resolved locally never passes through addKnownNode(), so the walk judges
// it here. The bound also covers nodeID, which the descent below writes.
condemn(*d, childHash, nodeID.getChildNodeID(branch));
return;
}
// The node's own full-below flag is not read here. The node object is shared by hash
// across maps and positions, so a flag another walk set says nothing about this
// position, and the position-keyed cache above has already missed for it.
else if (d->isInner())
{
mn.stack.push(se);
@@ -257,10 +280,14 @@ SHAMap::gmnProcessNodes(MissingNodes& mn, MissingNodes::StackEntry& se)
if (fullBelow)
{ // No partial node encountered below this node
node->setFullBelowGen(mn.generation);
// The node flag is written for the root alone, since the root position is the only one a
// shared node object can vouch for; see SHAMapInnerNode::fullBelowGen_.
if (nodeID.isRoot())
node->setFullBelowGen(mn.generation);
if (backed_)
{
f_.getFullBelowCache()->insert(node->getHash().asUInt256());
// Keyed by position as well as hash, so the entry answers only for this position.
f_.getFullBelowCache()->insert(node->getHash().asUInt256(), nodeID);
}
}
@@ -291,6 +318,25 @@ SHAMap::gmnProcessDeferredReads(MissingNodes& mn)
auto nodePtr = std::get<3>(deferredNode);
auto const& nodeHash = parent->getChildHash(branch);
// A deferred entry carries the position the walk held when it posted the read, so a branch
// below it is named here only where the tree has room for one.
if (nodePtr && nodePtr->isLeaf() && !isLeafDepth(parentID.getDepth()) &&
!belongsAt(parentID, branch, *nodePtr))
{
// The judgment the synchronous paths make, for a node an async read resolved. Skips
// rather than returns, since every posted read must be drained while `mn` is alive.
condemn(*nodePtr, nodeHash, parentID.getChildNodeID(branch));
continue;
}
// The depth bound gmnProcessNodes carries beside its leaf test, for a node an async read
// resolved. Skips for the same reason as above.
if (nodePtr && pastLeafDepth(parentID.getDepth(), *nodePtr))
{
condemn(*nodePtr, nodeHash, parentID.getChildNodeID(branch));
continue;
}
if (nodePtr)
{ // Got the node
nodePtr = parent->canonicalizeChild(branch, std::move(nodePtr));
@@ -301,6 +347,8 @@ SHAMap::gmnProcessDeferredReads(MissingNodes& mn)
}
else if ((mn.max > 0) && mn.missingHashes.insert(nodeHash).second)
{
// getChildNodeID() is safe here: gmnProcessNodes refuses to descend into an inner node
// at kLeafDepth, so a deferred parent sits at most one level above it.
mn.missingNodes.emplace_back(parentID.getChildNodeID(branch), nodeHash.asUInt256());
--mn.max;
}
@@ -311,17 +359,20 @@ SHAMap::gmnProcessDeferredReads(MissingNodes& mn)
mn.deferred = 0;
}
/**
* Get a list of node IDs and hashes for nodes that are part of this SHAMap
* but not available locally. The filter can hold alternate sources of
* nodes that are not permanently stored locally
*/
std::vector<std::pair<SHAMapNodeID, UInt256>>
SHAMap::getMissingNodes(int max, SHAMapSyncFilter const* filter)
{
XRPL_ASSERT(root_->getHash().isNonZero(), "xrpl::SHAMap::getMissingNodes : nonzero root hash");
XRPL_ASSERT(max > 0, "xrpl::SHAMap::getMissingNodes : valid max input");
if (!isValid())
{
// The root node's own hash, since getHash() unshares the tree on a zero hash.
JLOG(journal_.warn()) << "getMissingNodes called on an invalid map, root hash "
<< root_->getHash() << " seq " << ledgerSeq();
return {};
}
MissingNodes mn(
max,
filter,
@@ -354,6 +405,11 @@ SHAMap::getMissingNodes(int max, SHAMapSyncFilter const* filter)
{
gmnProcessNodes(mn, pos);
// The walk just invalidated the map. The loop stops descending here but falls through
// to the drain below, since every posted read must be drained while `mn` is alive.
if (!isValid())
break;
if (mn.max <= 0)
break;
@@ -383,6 +439,11 @@ SHAMap::getMissingNodes(int max, SHAMapSyncFilter const* filter)
if (mn.deferred != 0)
gmnProcessDeferredReads(mn);
// Reads are drained, so the map can be abandoned. What was collected belongs to a tree
// that cannot exist.
if (!isValid())
return {};
if (mn.max <= 0)
return std::move(mn.missingNodes);
@@ -391,12 +452,10 @@ SHAMap::getMissingNodes(int max, SHAMapSyncFilter const* filter)
if (mn.stack.empty() && !mn.resumes.empty())
{
// Recheck nodes we could not finish before
// Recheck nodes we could not finish before. A node's own full-below flag is not
// consulted: it is written for a root only, and a resumed node sits below one.
for (auto const& [innerNode, nodeId] : mn.resumes)
{
if (!innerNode->isFullBelow(mn.generation))
mn.stack.emplace(innerNode, nodeId, randInt(255), 0, true);
}
mn.stack.emplace(innerNode, nodeId, randInt(255), 0, true);
mn.resumes.clear();
}
@@ -416,6 +475,11 @@ SHAMap::getMissingNodes(int max, SHAMapSyncFilter const* filter)
} while (node != nullptr);
// addKnownNode() on another thread can write the verdict after the loop's own test, so the map
// is judged once more before the result is returned.
if (!isValid())
return {}; // LCOV_EXCL_LINE: only that other thread reaches this, so no test does
if (mn.missingNodes.empty())
clearSynching();
@@ -527,11 +591,19 @@ SHAMap::addRootNode(
XRPL_ASSERT(cowid_ >= 1, "xrpl::SHAMap::addRootNode : valid cowid");
XRPL_ASSERT(rootNode, "xrpl::SHAMap::addRootNode : non-null root node");
// we already have a root_ node
// A map syncs against one hash and installs a root once, so a root already held is a duplicate
// only once it hashes to the hash asked for.
if (root_->getHash().isNonZero())
{
JLOG(journal_.trace()) << "Got root node, already have one";
XRPL_ASSERT(root_->getHash() == hash, "xrpl::SHAMap::addRootNode : valid hash");
if (root_->getHash() != hash)
{
JLOG(journal_.warn()) << "Root node offered under hash " << hash
<< ", but the map holds " << root_->getHash();
return SHAMapAddNode::invalid();
}
return SHAMapAddNode::duplicate();
}
@@ -555,7 +627,7 @@ SHAMap::addRootNode(
Serializer s;
root_->serializeWithPrefix(s);
filter->gotNode(
false, root_->getHash(), ledgerSeq_, std::move(s.modData()), root_->getType());
false, root_->getHash(), ledgerSeq(), std::move(s.modData()), root_->getType());
}
return SHAMapAddNode::useful();
@@ -569,10 +641,6 @@ SHAMap::addKnownNode(
{
XRPL_ASSERT(!nodeID.isRoot(), "xrpl::SHAMap::addKnownNode : valid node");
XRPL_ASSERT(treeNode, "xrpl::SHAMap::addKnownNode : non-null tree node");
XRPL_ASSERT_IF(
treeNode->isLeaf(),
nodeID.isPrefixOf(leafKey(*treeNode)),
"xrpl::SHAMap::addKnownNode : leaf position consistent with node ID");
if (!isSynching())
{
@@ -580,13 +648,13 @@ SHAMap::addKnownNode(
return SHAMapAddNode::duplicate();
}
auto const generation = f_.getFullBelowCache()->getGeneration();
SHAMapNodeID currNodeID;
auto currNode = root_.get();
while (currNode->isInner() &&
!safeDowncast<SHAMapInnerNode*>(currNode)->isFullBelow(generation) &&
(currNodeID.getDepth() < nodeID.getDepth()))
// The descent reads no node's own full-below flag: a node object is shared by hash across
// maps and positions, so that flag answers for the root position alone. The position-keyed
// cache lookup in the loop is the memo that answers for a position below it.
while (currNode->isInner() && (currNodeID.getDepth() < nodeID.getDepth()))
{
auto const branch = selectBranch(currNodeID, nodeID.getNodeID());
auto inner = safeDowncast<SHAMapInnerNode*>(currNode);
@@ -598,7 +666,12 @@ SHAMap::addKnownNode(
}
auto childHash = inner->getChildHash(branch);
if (f_.getFullBelowCache()->touchIfExists(childHash.asUInt256()))
// The cache key carries the child's position beside its hash, so a hit answers for this
// child at this position and no other. The depth test runs first, so a node offered at
// kLeafDepth reaches the badDepth test below rather than being answered as a duplicate.
if (!isLeafDepth(currNodeID.getDepth() + 1) &&
f_.getFullBelowCache()->touchIfExists(childHash.asUInt256(), currNodeID, branch))
{
return SHAMapAddNode::duplicate();
}
@@ -606,6 +679,14 @@ SHAMap::addKnownNode(
auto prevNode = inner;
std::tie(currNode, currNodeID) = descend(inner, currNodeID, branch, filter);
if (!isValid())
{
// descend condemned the map. `childHash` was read before that descent, so the
// comparison below would read a stale value.
JLOG(journal_.warn()) << "Node " << nodeID << " cannot be hooked into an invalid map";
return SHAMapAddNode::invalid();
}
if (currNode != nullptr)
continue;
@@ -616,25 +697,33 @@ SHAMap::addKnownNode(
return SHAMapAddNode::invalid();
}
// Inner nodes must be at a level strictly less than 64
// but leaf nodes (while notionally at level 64) can be
// at any depth up to and including 64:
if ((currNodeID.getDepth() > kLeafDepth) ||
(treeNode->isInner() && currNodeID.getDepth() == kLeafDepth))
// Every node from the root down hash-verified to get here, so the requested root hash
// itself commits to a shape no tree can have. The verdict belongs to that hash.
bool const badDepth = treeNode->isInner() && isLeafDepth(currNodeID.getDepth());
SOMETIMES(badDepth, "xrpl::SHAMap::addKnownNode : map is invalid");
if (badDepth)
{
// Map is provably invalid
state_ = SHAMapState::Invalid;
return SHAMapAddNode::useful();
condemn(*treeNode, treeNode->getHash(), currNodeID);
return SHAMapAddNode::mapInvalidated();
}
if (currNodeID != nodeID)
// The data hashes to the child at currNodeID but is labeled nodeID, so it is not the node
// asked for. Only the label is wrong, so the map stays sound.
bool const badPosition = (currNodeID != nodeID);
SOMETIMES(badPosition, "xrpl::SHAMap::addKnownNode : node ID does not match its position");
if (badPosition)
{
// Either this node is broken or we didn't request it (yet)
JLOG(journal_.warn()) << "unable to hook node " << nodeID;
JLOG(journal_.info()) << " stuck at " << currNodeID;
JLOG(journal_.info()) << "got depth=" << nodeID.getDepth()
<< ", walked to= " << currNodeID.getDepth();
return SHAMapAddNode::useful();
JLOG(journal_.warn()) << "Unable to hook node " << nodeID << ", stuck at "
<< currNodeID;
return SHAMapAddNode::invalid();
}
// A leaf's own key names its position, and the hash test above ties this leaf to this
// parent, so the verdict belongs to the map. Below badPosition, since Invalid is terminal.
if (!belongsAt(nodeID, *treeNode))
{
condemn(*treeNode, treeNode->getHash(), nodeID);
return SHAMapAddNode::mapInvalidated();
}
if (backed_)
@@ -647,7 +736,7 @@ SHAMap::addKnownNode(
Serializer s;
treeNode->serializeWithPrefix(s);
filter->gotNode(
false, childHash, ledgerSeq_, std::move(s.modData()), treeNode->getType());
false, childHash, ledgerSeq(), std::move(s.modData()), treeNode->getType());
}
return SHAMapAddNode::useful();
@@ -766,7 +855,7 @@ SHAMap::hasLeafNode(UInt256 const& tag, SHAMapHash const& targetNodeHash) const
// Same kLeafDepth hazard as in visitDifferences above. That guard bounds the caller's own
// traversal, not the map queried here, and the loop below descends from this map's root
// independently, so this check is what keeps a malformed map from reaching getChildNodeID.
if (nodeID.getDepth() >= kLeafDepth)
if (isLeafDepth(nodeID.getDepth()))
{
// LCOV_EXCL_START
UNREACHABLE("xrpl::SHAMap::hasLeafNode : inner node at leaf depth");
@@ -802,7 +891,7 @@ SHAMap::getProofPath(UInt256 const& key) const
return {};
}
if (auto const& node = stack.top().first; !node || node->isInner() ||
if (auto const& node = stack.top(); !node || node->isInner() ||
intr_ptr::staticPointerCast<SHAMapLeafNode>(node)->peekItem()->key() != key)
{
JLOG(journal_.debug()) << "no path to " << key;
@@ -814,7 +903,7 @@ SHAMap::getProofPath(UInt256 const& key) const
while (!stack.empty())
{
Serializer s;
stack.top().first->serializeForWire(s);
stack.top()->serializeForWire(s);
path.emplace_back(std::move(s.modData()));
stack.pop();
}
@@ -849,8 +938,8 @@ SHAMap::verifyProofPath(UInt256 const& rootHash, UInt256 const& key, std::vector
// there. These nodes come off the wire, so a peer can still claim an inner one;
// reject it rather than passing this depth to selectBranch.
SOMETIMES(
depth >= kLeafDepth, "xrpl::SHAMap::verifyProofPath : inner at leaf depth");
if (depth >= kLeafDepth)
isLeafDepth(depth), "xrpl::SHAMap::verifyProofPath : inner at leaf depth");
if (isLeafDepth(depth))
return false;
auto const nodeId = SHAMapNodeID::createID(depth, key);
@@ -859,10 +948,9 @@ SHAMap::verifyProofPath(UInt256 const& rootHash, UInt256 const& key, std::vector
}
else
{
// The hash chain up to rootHash only proves this leaf sits where the path claims,
// not that it is the leaf for `key`: a peer could substitute any other leaf whose
// subtree hashes to the same value at every level above it. Checking the terminal
// leaf's own key is what ties the proof to `key` specifically.
// The hash chain up to rootHash proves this leaf sits where the path claims. Any
// leaf whose subtree hashes the same at every level above satisfies that chain, so
// the terminal leaf's own key is what ties the proof to `key`.
if (leafKey(*node) != key)
return false;
+35 -2
View File
@@ -16,6 +16,7 @@
#include <xrpl/ledger/helpers/DelegateHelpers.h>
#include <xrpl/ledger/helpers/NFTokenHelpers.h>
#include <xrpl/ledger/helpers/OfferHelpers.h>
#include <xrpl/ledger/helpers/ProposalHelpers.h>
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
#include <xrpl/ledger/helpers/SponsorHelpers.h>
#include <xrpl/protocol/AccountID.h>
@@ -327,10 +328,9 @@ Transactor::preflight2(PreflightContext const& ctx)
auto const sigValid = checkValidity(ctx.registry.get().getHashRouter(), ctx.tx, ctx.rules);
if (sigValid.first == Validity::SigBad)
{ // LCOV_EXCL_START
{
JLOG(ctx.j.debug()) << "preflight2: bad signature. " << sigValid.second;
return temINVALID;
// LCOV_EXCL_STOP
}
// Do not add any checks after this point that are relevant for
@@ -755,6 +755,19 @@ Transactor::checkSeqProxy(ReadView const& view, STTx const& tx, beast::Journal j
<< "a_seq=" << aSeq << " t_seq=" << tSeqProx;
return tefNO_TICKET;
}
// While a TransactionProposal keyed to this Ticket exists, the
// Ticket is reserved: only the proposal's own proposed transaction
// may consume it, so unrelated activity cannot invalidate the
// proposal while signatures are being collected (On-Chain Cosigner spec §4.2.1).
// Cancelling the proposal frees the Ticket, so this is a retry, not
// a final failure. No proposal can exist before the Cosign amendment,
// so the gate only makes that dependency explicit.
if (view.rules().enabled(featureCosign) && !proposal::canConsumeTicket(view, tx))
{
JLOG(j.trace()) << "applyTransaction: ticket " << tSeqProx << " is reserved";
return terTICKET_RESERVED;
}
}
return tesSUCCESS;
@@ -866,8 +879,28 @@ Transactor::ticketDelete(
// Update the Ticket owner's reserve.
decreaseOwnerCountForObject(view, sleAccount, sleTicket, 1, j);
std::uint32_t const ticketSeq{(*sleTicket)[sfTicketSequence]};
// Remove Ticket from ledger.
view.erase(sleTicket);
// Once the Ticket is gone, a TransactionProposal keyed to it can never
// execute: its proposed transaction would fail with tefNO_TICKET. Clean
// up the stale proposal and release its Owner's reserve (On-Chain Cosigner spec §4.5).
// The reservation check in checkSeqProxy means this is reached only by
// the proposal's own proposed transaction executing (or failing with a
// claimed-fee tec), or by AccountDelete sweeping the target account's
// Tickets. No proposal can exist before the Cosign amendment, so the gate
// only makes that dependency explicit.
if (view.rules().enabled(featureCosign))
{
if (auto const sleProposal = view.peek(keylet::txProposal(account, ticketSeq)))
{
if (TER const ter = proposal::deleteProposal(view, sleProposal, j); !isTesSuccess(ter))
return ter; // LCOV_EXCL_LINE
}
}
return tesSUCCESS;
}
@@ -9,6 +9,7 @@
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Asset.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Issue.h>
@@ -22,6 +23,7 @@
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STNumber.h> // IWYU pragma: keep
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/STVector256.h>
#include <xrpl/protocol/SystemParameters.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFormats.h>
@@ -1010,6 +1012,78 @@ ValidClawback::finalize(
//------------------------------------------------------------------------------
// Whether `entry` is the trust line or MPToken through which `account` holds
// `asset`. XRP has no such entry.
static bool
isHolding(SLE const& entry, Asset const& asset, AccountID const& account)
{
return asset.visit(
[&](Issue const& issue) {
return !isXRP(issue) && keylet::trustLine(account, issue).check(entry);
},
[&](MPTIssue const& issue) {
return keylet::mptoken(issue.getMptID(), account).check(entry);
});
}
// Whether `entry`, linked into the owner directory of `pseudo`, belongs to the
// AMM, Vault or LoanBroker that the pseudo-account was created for.
static bool
isOwnedByPseudoAccount(ReadView const& view, SLE const& pseudo, SLE const& entry)
{
auto const pseudoAcctID = pseudo[sfAccount];
if (pseudo.isFieldPresent(sfAMMID))
{
if (entry.getType() == ltAMM)
return entry.key() == pseudo[sfAMMID];
auto const amm = view.read(keylet::amm(pseudo[sfAMMID]));
if (!amm)
return false; // LCOV_EXCL_LINE
if (isHolding(entry, (*amm)[sfAsset], pseudoAcctID) ||
isHolding(entry, (*amm)[sfAsset2], pseudoAcctID))
return true;
// The AMM issues LP tokens, so each liquidity provider's trust line
// links into its directory.
if (entry.getType() != ltRIPPLE_STATE)
return false;
auto const low = entry[sfLowLimit].getIssuer();
auto const high = entry[sfHighLimit].getIssuer();
auto const lpCurrency = (*amm)[sfLPTokenBalance].get<Issue>().currency;
return (low == pseudoAcctID || high == pseudoAcctID) &&
entry.key() == keylet::trustLine(low, high, lpCurrency).key;
}
if (pseudo.isFieldPresent(sfVaultID))
{
if (entry.getType() == ltLOAN_BROKER)
return entry[sfVaultID] == pseudo[sfVaultID];
auto const vault = view.read(keylet::vault(pseudo[sfVaultID]));
if (!vault)
return false; // LCOV_EXCL_LINE
return keylet::mptokenIssuance((*vault)[sfShareMPTID]).check(entry) ||
isHolding(entry, (*vault)[sfAsset], pseudoAcctID);
}
if (pseudo.isFieldPresent(sfLoanBrokerID))
{
if (entry.getType() == ltLOAN)
return entry[sfLoanBrokerID] == pseudo[sfLoanBrokerID];
auto const broker = view.read(keylet::loanBroker(pseudo[sfLoanBrokerID]));
if (!broker)
return false; // LCOV_EXCL_LINE
auto const vault = view.read(keylet::vault((*broker)[sfVaultID]));
return vault && isHolding(entry, (*vault)[sfAsset], pseudoAcctID);
}
// A new pseudo-account kind must say what it owns.
return false; // LCOV_EXCL_LINE
}
void
ValidPseudoAccounts::visitEntry(bool isDelete, SLE::ConstRef before, SLE::ConstRef after)
{
@@ -1019,6 +1093,33 @@ ValidPseudoAccounts::visitEntry(bool isDelete, SLE::ConstRef before, SLE::ConstR
return;
}
// Only owner directory pages carry sfOwner; book directories don't.
if (after && after->getType() == ltDIR_NODE && after->isFieldPresent(sfOwner))
{
auto const& afterIndexes = after->getFieldV256(sfIndexes);
std::vector<uint256> added;
if (!before)
{
added.assign(afterIndexes.begin(), afterIndexes.end());
}
else
{
auto const& beforeIndexes = before->getFieldV256(sfIndexes);
for (auto const& index : afterIndexes)
{
if (!std::ranges::contains(beforeIndexes, index))
added.push_back(index);
}
}
if (!added.empty())
{
auto& owned = ownerDirAdditions_[after->getAccountID(sfOwner)];
owned.insert(owned.end(), added.begin(), added.end());
}
return;
}
if (after && after->getType() == ltACCOUNT_ROOT)
{
bool const isPseudo = [&] {
@@ -1100,6 +1201,39 @@ ValidPseudoAccounts::finalize(
if (enforce)
return false;
}
// Pre-fixCleanup3_5_0: what a pseudo-account owns is not checked.
// Post-fixCleanup3_5_0: it may own only what belongs to its AMM, Vault or
// LoanBroker.
if (!view.rules().enabled(fixCleanup3_5_0))
return true;
for (auto const& [owner, indexes] : ownerDirAdditions_)
{
auto const root = view.read(keylet::account(owner));
if (!root || !isPseudoAccount(root))
continue;
for (auto const& index : indexes)
{
auto const sle = view.read(keylet::unchecked(index));
if (!sle)
{
JLOG(j.fatal()) << "Invariant failed: pseudo-account " << toBase58(owner)
<< " owner directory links a missing object " << to_string(index);
return false;
}
if (!isOwnedByPseudoAccount(view, *root, *sle))
{
JLOG(j.fatal()) << "Invariant failed: pseudo-account " << toBase58(owner)
<< " may not own " << ledgerEntryTypeName(*sle) << " "
<< to_string(index);
return false;
}
}
}
return true;
}
@@ -5,8 +5,8 @@
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/helpers/LendingHelpers.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/ledger/helpers/VaultHelpers.h>
#include <xrpl/protocol/Asset.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
@@ -160,7 +160,7 @@ ValidLoanBroker::finalize(
roundToAsset(
Asset{vault->at(sfAsset)},
debtTotal,
getAssetsTotalScale(vault),
getVaultScale(vault),
Number::RoundingMode::TowardsZero) != beast::kZero)
{
JLOG(j.fatal())
+19 -13
View File
@@ -339,6 +339,7 @@ ValidMPTIssuance::finalize(
// Cap on MPToken creates and deletes while featureLendingProtocol is enabled.
// - LoanSet: at most two creates and no deletes.
// - VaultWithdraw: at most one create and one delete.
// - LoanBrokerDelete (post-fixCleanup3_5_0): at most one create and one delete.
// - Other MayAuthorizeMpt types: created + deleted <= 1.
// - MustAuthorizeMpt still requires exactly one create or delete below.
auto const mptokensExceedAuthorizeCap = [&] {
@@ -351,6 +352,10 @@ ValidMPTIssuance::finalize(
if (txnType == ttVAULT_WITHDRAW)
return mptokensCreated_ > 1 || mptokensDeleted_ > 1;
}
// The cover payout may create the owner's MPToken while the
// pseudo-account's MPToken is deleted.
if (rules.enabled(fixCleanup3_5_0) && txnType == ttLOAN_BROKER_DELETE)
return mptokensCreated_ > 1 || mptokensDeleted_ > 1;
return (mptokensCreated_ + mptokensDeleted_) > 1;
};
if (mptokensExceedAuthorizeCap())
@@ -1018,24 +1023,25 @@ ValidMPTTransfer::finalize(
continue;
}
// These transactions are recovery/settlement paths. They may move an
// existing MPT position even after the issuer clears CanTransfer, so
// holders are not trapped in AMM, vault, or loan protocol accounts.
auto const waivesCanTransfer = txnType == ttAMM_WITHDRAW ||
(view.rules().enabled(fixCleanup3_2_0) &&
(txnType == ttVAULT_WITHDRAW || txnType == ttLOAN_BROKER_COVER_WITHDRAW ||
txnType == ttLOAN_PAY));
auto const canTransfer = sleIssuance->isFlag(lsfMPTCanTransfer) || waivesCanTransfer;
auto const canTrade = sleIssuance->isFlag(lsfMPTCanTrade);
auto const reqAuth = sleIssuance->isFlag(lsfMPTRequireAuth);
// This issuance is the LoanManage default's own vault asset, so the
// broker/vault freeze exemption applies to it -- an unrelated MPT
// issuance the same accounts happen to hold is still caught.
// broker/vault freeze and CanTransfer exemptions apply to it -- an
// unrelated MPT the same accounts happen to hold is still caught.
bool const isLoanDefaultAsset = loanDefaultAccounts &&
loanDefaultAccounts->asset.holds<MPTIssue>() &&
loanDefaultAccounts->asset.get<MPTIssue>().getMptID() == mptID;
// These transactions settle an existing MPT position out of a protocol
// account. They do not require CanTransfer.
auto const waivesCanTransfer = txnType == ttAMM_WITHDRAW ||
(view.rules().enabled(fixCleanup3_2_0) &&
(txnType == ttVAULT_WITHDRAW || txnType == ttLOAN_BROKER_COVER_WITHDRAW ||
txnType == ttLOAN_PAY)) ||
(view.rules().enabled(fixCleanup3_5_0) &&
(isLoanDefaultAsset || txnType == ttLOAN_BROKER_DELETE));
auto const canTransfer = sleIssuance->isFlag(lsfMPTCanTransfer) || waivesCanTransfer;
auto const canTrade = sleIssuance->isFlag(lsfMPTCanTrade);
auto const reqAuth = sleIssuance->isFlag(lsfMPTRequireAuth);
for (auto const& [account, value] : values)
{
// Classify each account as a sender or receiver based on whether their MPTAmount
+226 -81
View File
@@ -7,12 +7,14 @@
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/ledger/helpers/VaultHelpers.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/Rate.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STLedgerEntry.h>
@@ -63,6 +65,9 @@ ValidVault::Vault::make(SLE const& from)
self.assetsAvailable = from.at(sfAssetsAvailable);
self.assetsMaximum = from.at(sfAssetsMaximum);
self.lossUnrealized = from.at(sfLossUnrealized);
self.assetsDeployed = from[~sfAssetsDeployed].value_or(Number{0});
self.yieldUnrealized = from[~sfYieldUnrealized].value_or(Number{0});
self.version = decodeVaultVersion(from[~sfLEVersion]);
self.vaultKind = from[~sfVaultKind];
self.subscriptionDate = from[~sfSubscriptionDate];
self.redemptionDate = from[~sfRedemptionDate];
@@ -408,6 +413,70 @@ ValidVault::computeVaultMinScale(DeltaInfo const& vaultDelta, Rules const& rules
return computeCoarsestScale({vaultDelta, totalDelta, availableDelta});
}
bool
ValidVault::checkTotalsAddUp(
Vault const& afterVault,
Vault const& beforeVault,
Asset const& vaultAsset,
bool fix340Enabled,
std::int32_t minScale,
Number const& roundedVaultDelta,
Number const& exactVaultDelta,
char const* verb,
beast::Journal const& j)
{
bool result = true;
// FixedPrecision: the stored AssetsTotal is a Downward-floored cache
// (AA + AD), not an exact running total, so comparing its delta against
// the real transfer can be off by more than one unit purely from
// independent floor-rounding of the two snapshots -- not an accounting
// bug. The stored-cache-equals-derived-total check elsewhere in finalize,
// plus the AssetsDeployed-only-changed-by-lending-transactors check,
// already cover AssetsTotal; only compare it here on Legacy/CashBasis,
// where AssetsTotal is written exactly alongside AA.
if (afterVault.version != VaultVersion::FixedPrecision)
{
auto const assetTotalDelta =
roundToAsset(vaultAsset, afterVault.assetsTotal - beforeVault.assetsTotal, minScale);
bool const totalAddsUp = fix340Enabled
? agreesWithinOneUnit(assetTotalDelta, roundedVaultDelta, vaultAsset, minScale)
: assetTotalDelta == roundedVaultDelta;
if (!totalAddsUp)
{
JLOG(j.fatal()) << "Invariant failed: " << verb
<< " and assets outstanding must add up";
result = false;
}
}
auto const assetAvailableDelta = roundToAsset(
vaultAsset, afterVault.assetsAvailable - beforeVault.assetsAvailable, minScale);
// FixedPrecision: AssetsAvailable is the exact, single writer
// (adjustVaultBalances)-managed balance, so its delta must match the real
// transfer exactly, with no rounding tolerance -- compare the exact
// deltas, not the values rounded to the posterior AssetsTotal scale.
bool const availableAddsUp = [&] {
if (afterVault.version == VaultVersion::FixedPrecision)
{
return (afterVault.assetsAvailable - beforeVault.assetsAvailable) == exactVaultDelta;
}
if (fix340Enabled)
{
return agreesWithinOneUnit(
assetAvailableDelta, roundedVaultDelta, vaultAsset, minScale);
}
return assetAvailableDelta == roundedVaultDelta;
}();
if (!availableAddsUp)
{
JLOG(j.fatal()) << "Invariant failed: " << verb << " and assets available must add up";
result = false;
}
return result;
}
bool
ValidVault::finalize(
STTx const& tx,
@@ -418,6 +487,7 @@ ValidVault::finalize(
{
bool const enforce = view.rules().enabled(featureSingleAssetVault);
bool const fix340Enabled = view.rules().enabled(fixCleanup3_4_0);
bool const fix350Enabled = view.rules().enabled(fixCleanup3_5_0);
if (!isTesSuccess(ret))
return true; // Do not perform checks
@@ -585,6 +655,13 @@ ValidVault::finalize(
"vault must have no assets available";
result = false;
}
if (afterVault.version == VaultVersion::FixedPrecision &&
afterVault.assetsDeployed != kZero)
{
JLOG(j.fatal()) << "Invariant failed: updated zero sized "
"vault must have no AssetsDeployed";
result = false;
}
}
else if (updatedShares->sharesTotal > updatedShares->sharesMaximum)
{
@@ -609,6 +686,14 @@ ValidVault::finalize(
else
{
bool const gapExceeded = [&] {
if (afterVault.version == VaultVersion::FixedPrecision)
{
// FixedPrecision: both LossUnrealized and AssetsDeployed are exact
// (never rounded), so compare them directly. AssetsTotal is
// just a rounded cache of AssetsAvailable + AssetsDeployed and
// is not the reference this check needs.
return afterVault.lossUnrealized > afterVault.assetsDeployed;
}
if (!fix340Enabled)
{
return afterVault.lossUnrealized >
@@ -631,6 +716,43 @@ ValidVault::finalize(
}
}
if (afterVault.version == VaultVersion::FixedPrecision)
{
if (afterVault.assetsDeployed < kZero)
{
JLOG(j.fatal()) << "Invariant failed: AssetsDeployed must not be negative";
result = false;
}
else if (
Number(STAmount{afterVault.asset, afterVault.assetsDeployed}) !=
afterVault.assetsDeployed)
{
JLOG(j.fatal()) //
<< "Invariant failed: AssetsDeployed must be exactly representable "
"at the vault asset's precision";
result = false;
}
Number const expectedAssetsTotal = [&] {
NumberRoundModeGuard const rg(Number::RoundingMode::Downward);
return Number(
STAmount{afterVault.asset, afterVault.assetsAvailable + afterVault.assetsDeployed});
}();
if (afterVault.assetsTotal != expectedAssetsTotal)
{
JLOG(j.fatal()) //
<< "Invariant failed: stored AssetsTotal must equal AssetsAvailable "
"plus AssetsDeployed, rounded Downward";
result = false;
}
if (afterVault.yieldUnrealized < kZero)
{
JLOG(j.fatal()) << "Invariant failed: YieldUnrealized must not be negative";
result = false;
}
}
if (fix340Enabled && afterVault.lossUnrealized < kZero)
{
JLOG(j.fatal()) << "Invariant failed: loss unrealized must not be negative";
@@ -659,6 +781,18 @@ ValidVault::finalize(
return !enforce; // That's all we can do here
}
// ttLOAN_SET, ttLOAN_PAY and ttLOAN_MANAGE are the only transactors allowed to
// write AssetsDeployed and YieldUnrealized; any new transactor that writes
// either field must be added to both allow-lists below.
if (afterVault.version == VaultVersion::FixedPrecision && !beforeVault_.empty() &&
afterVault.assetsDeployed != beforeVault_[0].assetsDeployed && txnType != ttLOAN_SET &&
txnType != ttLOAN_PAY && txnType != ttLOAN_MANAGE)
{
JLOG(j.fatal()) << //
"Invariant failed: vault transaction must not change AssetsDeployed";
result = false;
}
if (!beforeVault_.empty() && afterVault.lossUnrealized != beforeVault_[0].lossUnrealized &&
txnType != ttLOAN_MANAGE && txnType != ttLOAN_PAY)
{
@@ -668,6 +802,16 @@ ValidVault::finalize(
result = false;
}
if (afterVault.version == VaultVersion::FixedPrecision && !beforeVault_.empty() &&
afterVault.yieldUnrealized != beforeVault_[0].yieldUnrealized && txnType != ttLOAN_SET &&
txnType != ttLOAN_PAY && txnType != ttLOAN_MANAGE)
{
JLOG(j.fatal()) << //
"Invariant failed: vault transaction must not change yield "
"unrealized";
result = false;
}
// Immutability of VaultKind, SubscriptionDate and RedemptionDate is enforced by
// NoModifiedUnmodifiableFields in InvariantCheck.cpp.
@@ -716,7 +860,9 @@ ValidVault::finalize(
}
if (afterVault.assetsAvailable != kZero || afterVault.assetsTotal != kZero ||
afterVault.lossUnrealized != kZero || updatedShares->sharesTotal != 0)
afterVault.lossUnrealized != kZero || updatedShares->sharesTotal != 0 ||
(afterVault.version == VaultVersion::FixedPrecision &&
(afterVault.assetsDeployed != kZero || afterVault.yieldUnrealized != kZero)))
{
JLOG(j.fatal()) //
<< "Invariant failed: created vault must be empty";
@@ -880,7 +1026,17 @@ ValidVault::finalize(
result = false;
}
if (vaultDeltaAssets <= kZero)
// FixedPrecision: AssetsAvailable is exact and the pseudo-account
// delta is exact; rounding either one down to the posterior
// AssetsTotal scale before the sign check can turn a legitimate
// sub-unit deposit into a false zero. Compare the exact deltas
// instead; Legacy/CashBasis keep the rounded comparison.
bool const isFixedPrecision = afterVault.version == VaultVersion::FixedPrecision;
Number const exactVaultDeltaAssets = maybeVaultDeltaAssets->delta;
Number const signCheckDeltaAssets =
isFixedPrecision ? exactVaultDeltaAssets : vaultDeltaAssets;
if (signCheckDeltaAssets <= kZero)
{
JLOG(j.fatal()) << //
"Invariant failed: deposit must increase vault balance";
@@ -976,29 +1132,17 @@ ValidVault::finalize(
result = false;
}
auto const assetTotalDelta = roundToAsset(
vaultAsset, afterVault.assetsTotal - beforeVault.assetsTotal, minScale);
bool const totalAddsUp = fix340Enabled
? agreesWithinOneUnit(assetTotalDelta, vaultDeltaAssets, vaultAsset, minScale)
: assetTotalDelta == vaultDeltaAssets;
if (!totalAddsUp)
{
JLOG(j.fatal())
<< "Invariant failed: deposit and assets outstanding must add up";
if (!checkTotalsAddUp(
afterVault,
beforeVault,
vaultAsset,
fix340Enabled,
minScale,
vaultDeltaAssets,
exactVaultDeltaAssets,
"deposit",
j))
result = false;
}
auto const assetAvailableDelta = roundToAsset(
vaultAsset, afterVault.assetsAvailable - beforeVault.assetsAvailable, minScale);
bool const availableAddsUp = fix340Enabled
? agreesWithinOneUnit(
assetAvailableDelta, vaultDeltaAssets, vaultAsset, minScale)
: assetAvailableDelta == vaultDeltaAssets;
if (!availableAddsUp)
{
JLOG(j.fatal()) << "Invariant failed: deposit and assets available must add up";
result = false;
}
return result;
}
@@ -1054,7 +1198,17 @@ ValidVault::finalize(
auto const vaultPseudoDeltaAssets =
roundToAsset(vaultAsset, vaultDeltaAssets.delta, minScale);
if (!zeroDeltaIsLegitimate && vaultPseudoDeltaAssets >= kZero)
// FixedPrecision: AssetsAvailable is exact and the pseudo-account
// delta is exact; rounding either one down to the posterior
// AssetsTotal scale before the sign check can turn a legitimate
// sub-unit withdrawal into a false zero and reject it. Compare
// the exact deltas instead; Legacy/CashBasis keep the rounded
// comparison.
bool const isFixedPrecision = afterVault.version == VaultVersion::FixedPrecision;
Number const signCheckDeltaAssets =
isFixedPrecision ? vaultDeltaAssets.delta : vaultPseudoDeltaAssets;
if (!zeroDeltaIsLegitimate && signCheckDeltaAssets >= kZero)
{
JLOG(j.fatal()) << "Invariant failed: withdrawal must decrease vault balance";
result = false;
@@ -1144,6 +1298,18 @@ ValidVault::finalize(
auto const localPseudoDeltaAssets =
roundToAsset(vaultAsset, vaultPseudoDeltaAssets, localMinScale);
auto const rate = transferRate(view, vaultAsset);
bool const feeAdjustedWithdrawal = fix350Enabled && distinctDestination &&
!vaultAsset.native() && tx[sfAccount] != vaultAsset.getIssuer() &&
rate != kParityRate;
auto expectedDestinationDelta = localPseudoDeltaAssets * -1;
if (feeAdjustedWithdrawal)
{
expectedDestinationDelta =
subtractTransferFee(
STAmount{vaultAsset, expectedDestinationDelta}, rate)
.value();
}
// For IOU assets near a precision boundary the destination's STAmount
// exponent can shift, making part of the sent value unrepresentable at
// the receiver's new scale — that portion is irreversibly absorbed by the
@@ -1152,7 +1318,8 @@ ValidVault::finalize(
// the destination's scale. Floor rounding is used so that values exactly
// at the step boundary are not mistakenly dismissed. Any representable
// discrepancy indicates a real accounting bug and must be caught.
auto const destroyedIsSubUlp = tolerateZeroDelta &&
auto const destroyedIsSubUlp = !feeAdjustedWithdrawal &&
tolerateZeroDelta &&
roundToAsset(
vaultAsset,
vaultDeltaAssets.delta * -1 - destinationDelta.delta,
@@ -1160,11 +1327,11 @@ ValidVault::finalize(
Number::RoundingMode::Downward) == kZero;
bool const withdrawAddsUp = fix340Enabled
? agreesWithinOneUnit(
localPseudoDeltaAssets * -1,
expectedDestinationDelta,
roundedDestinationDelta,
vaultAsset,
localMinScale)
: localPseudoDeltaAssets * -1 == roundedDestinationDelta;
: expectedDestinationDelta == roundedDestinationDelta;
if (!destroyedIsSubUlp && !withdrawAddsUp)
{
JLOG(j.fatal()) << "Invariant failed: " << //
@@ -1205,33 +1372,17 @@ ValidVault::finalize(
result = false;
}
auto const assetTotalDelta = roundToAsset(
vaultAsset, afterVault.assetsTotal - beforeVault.assetsTotal, minScale);
// Note, vaultBalance is negative (see check above)
bool const totalAddsUp = fix340Enabled
? agreesWithinOneUnit(
assetTotalDelta, vaultPseudoDeltaAssets, vaultAsset, minScale)
: assetTotalDelta == vaultPseudoDeltaAssets;
if (!totalAddsUp)
{
JLOG(j.fatal())
<< "Invariant failed: withdrawal and assets outstanding must add up";
if (!checkTotalsAddUp(
afterVault,
beforeVault,
vaultAsset,
fix340Enabled,
minScale,
vaultPseudoDeltaAssets,
vaultDeltaAssets.delta,
"withdrawal",
j))
result = false;
}
auto const assetAvailableDelta = roundToAsset(
vaultAsset, afterVault.assetsAvailable - beforeVault.assetsAvailable, minScale);
bool const availableAddsUp = fix340Enabled
? agreesWithinOneUnit(
assetAvailableDelta, vaultPseudoDeltaAssets, vaultAsset, minScale)
: assetAvailableDelta == vaultPseudoDeltaAssets;
if (!availableAddsUp)
{
JLOG(j.fatal())
<< "Invariant failed: withdrawal and assets available must add up";
result = false;
}
return result;
}
@@ -1263,39 +1414,33 @@ ValidVault::finalize(
computeVaultMinScale(*maybeVaultDeltaAssets, view.rules());
auto const vaultDeltaAssets =
roundToAsset(vaultAsset, maybeVaultDeltaAssets->delta, minScale);
if (vaultDeltaAssets >= kZero)
// FixedPrecision: AssetsAvailable is exact and the
// pseudo-account delta is exact; rounding either one down
// to the posterior AssetsTotal scale before the sign
// check can turn a legitimate sub-unit clawback into a
// false zero and reject it. Compare the exact deltas
// instead; Legacy/CashBasis keep the rounded comparison.
bool const isFixedPrecision =
afterVault.version == VaultVersion::FixedPrecision;
Number const signCheckDeltaAssets =
isFixedPrecision ? maybeVaultDeltaAssets->delta : vaultDeltaAssets;
if (signCheckDeltaAssets >= kZero)
{
JLOG(j.fatal()) << "Invariant failed: clawback must decrease vault balance";
result = false;
}
auto const assetsTotalDelta = roundToAsset(
vaultAsset, afterVault.assetsTotal - beforeVault.assetsTotal, minScale);
bool const totalAddsUp = fix340Enabled
? agreesWithinOneUnit(
assetsTotalDelta, vaultDeltaAssets, vaultAsset, minScale)
: assetsTotalDelta == vaultDeltaAssets;
if (!totalAddsUp)
{
JLOG(j.fatal()) << //
"Invariant failed: clawback and assets outstanding must add up";
if (!checkTotalsAddUp(
afterVault,
beforeVault,
vaultAsset,
fix340Enabled,
minScale,
vaultDeltaAssets,
maybeVaultDeltaAssets->delta,
"clawback",
j))
result = false;
}
auto const assetAvailableDelta = roundToAsset(
vaultAsset,
afterVault.assetsAvailable - beforeVault.assetsAvailable,
minScale);
bool const availableAddsUp = fix340Enabled
? agreesWithinOneUnit(
assetAvailableDelta, vaultDeltaAssets, vaultAsset, minScale)
: assetAvailableDelta == vaultDeltaAssets;
if (!availableAddsUp)
{
JLOG(j.fatal()) << //
"Invariant failed: clawback and assets available must add up";
result = false;
}
}
else if (!isVaultEmpty(beforeVault))
{
+1 -1
View File
@@ -177,7 +177,7 @@ AMMLiquidity<TIn, TOut>::getOffer(ReadView const& view, std::optional<Quality> c
return maxOffer(balances);
}
if (auto const amounts =
changeSpotPriceQuality(balances, *clobQuality, tradingFee_, view.rules(), j_))
changeSpotPriceQuality(balances, *clobQuality, tradingFee_, j_))
{
return AMMOffer<TIn, TOut>(*this, *amounts, balances, Quality{*amounts});
}
+164 -70
View File
@@ -24,7 +24,6 @@
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/Quality.h>
#include <xrpl/protocol/Rate.h>
#include <xrpl/protocol/Rules.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/TER.h>
@@ -83,6 +82,11 @@ protected:
std::optional<AMMLiquidity<TIn, TOut>> ammLiquidity_;
beast::Journal const j_;
Asset const strandDeliver_;
// MaximumAmount of book_.in if it is an MPT. Taking an MPT offer has the
// issuer send TakerPays to the owner, and a send above the cap is rejected
// (isMPTOverflow). forEachOffer limits the offer's input to the cap so it
// fills partially instead of failing the strand.
std::optional<TIn> maxIn_;
struct Cache
{
@@ -106,6 +110,12 @@ private:
, j_(ctx.j)
, strandDeliver_(ctx.strandDeliver)
{
if constexpr (std::is_same_v<TIn, MPTAmount>)
{
if (auto const max = maxMPTAmount(ctx.view, in.get<MPTIssue>().getMptID()))
maxIn_.emplace(*max);
}
if (auto const ammSle = ctx.view.read(keylet::amm(in, out));
ammSle && ammSle->getFieldAmount(sfLPTokenBalance) != beast::kZero)
{
@@ -344,8 +354,7 @@ public:
Quality const& ofrQ,
DebtDirection prevStepDir,
WaiveTransferFee waiveFee,
OfferType,
Rules const&) const
OfferType) const
{
// Charge the offer owner, not the sender
// Charge a fee even if the owner is the same as the issuer
@@ -516,8 +525,7 @@ public:
Quality const& ofrQ,
DebtDirection prevStepDir,
WaiveTransferFee waiveFee,
OfferType offerType,
Rules const& rules) const
OfferType offerType) const
{
// Offer x-ing does not charge a transfer fee when the offer's owner
// is the same as the strand dst. It is important that
@@ -529,10 +537,6 @@ public:
// Single path AMM offer has to factor in the transfer in rate
// when calculating the upper bound quality and the quality function
// because single path AMM's offer quality is not constant.
if (!rules.enabled(fixAMMv1_1))
{
return ofrQ;
}
if (offerType == OfferType::Clob ||
(this->ammLiquidity_ && this->ammLiquidity_->multiPath()))
{
@@ -584,7 +588,7 @@ BookStep<TIn, TOut, TDerived>::qualityUpperBound(ReadView const& v, DebtDirectio
: WaiveTransferFee::No;
Quality const q = static_cast<TDerived const*>(this)->adjustQualityWithFees(
v, std::get<Quality>(*res), prevStepDir, waiveFee, std::get<OfferType>(*res), v.rules());
v, std::get<Quality>(*res), prevStepDir, waiveFee, std::get<OfferType>(*res));
return {q, dir};
}
@@ -603,7 +607,7 @@ BookStep<TIn, TOut, TDerived>::getQualityFunc(ReadView const& v, DebtDirection p
{
auto static const kQOne = Quality{STAmount::kURateOne};
auto const q = static_cast<TDerived const*>(this)->adjustQualityWithFees(
v, kQOne, prevStepDir, WaiveTransferFee::Yes, OfferType::Amm, v.rules());
v, kQOne, prevStepDir, WaiveTransferFee::Yes, OfferType::Amm);
if (q == kQOne)
return {res, dir};
QualityFunction qf{q, QualityFunction::CLOBLikeTag{}};
@@ -618,8 +622,7 @@ BookStep<TIn, TOut, TDerived>::getQualityFunc(ReadView const& v, DebtDirection p
// always has quality set
prevStepDir,
WaiveTransferFee::No,
OfferType::Clob,
v.rules());
OfferType::Clob);
return {QualityFunction{q, QualityFunction::CLOBLikeTag{}}, dir};
}
@@ -630,6 +633,38 @@ BookStep<TIn, TOut, TDerived>::offersUsed() const
return offersUsed_;
}
// Limit the offer to the given input (net of the taker's transfer fee), and
// the step output and owner's charge with it. The caller sets stpAmt.in.
template <class TIn, class TOut, class Offer>
static void
limitOfferIn(
Offer const& offer,
TAmounts<TIn, TOut>& ofrAmt,
TAmounts<TIn, TOut>& stpAmt,
TOut& ownerGives,
std::uint32_t transferRateOut,
TIn const& inLmt)
{
// It turns out we can prevent order book blocking by (strictly)
// rounding down the ceil_in() result. By rounding down we guarantee
// that the quality of an offer left in the ledger is as good or
// better than the quality of the containing order book page.
//
// This adjustment changes transaction outcomes, so it must be made
// under an amendment.
ofrAmt = offer.limitIn(ofrAmt, inLmt, /* roundUp */ false);
stpAmt.out = ofrAmt.out;
// Round up for MPT output so the offer owner pays the full
// ceil(amount × rate) fee, matching direct Payment semantics. IOU uses
// floating-point arithmetic so the floor/ceil distinction is sub-epsilon
// there; preserve the historical false to avoid changing IOU behavior.
ownerGives = mulRatio(
ofrAmt.out,
transferRateOut,
QUALITY_ONE,
/*roundUp*/ std::is_same_v<TOut, MPTAmount>);
}
// Adjust the offer amount and step amount subject to the given input limit
template <class TIn, class TOut, class Offer>
static void
@@ -646,24 +681,7 @@ limitStepIn(
{
stpAmt.in = limit;
auto const inLmt = mulRatio(stpAmt.in, QUALITY_ONE, transferRateIn, /*roundUp*/ false);
// It turns out we can prevent order book blocking by (strictly)
// rounding down the ceil_in() result. By rounding down we guarantee
// that the quality of an offer left in the ledger is as good or
// better than the quality of the containing order book page.
//
// This adjustment changes transaction outcomes, so it must be made
// under an amendment.
ofrAmt = offer.limitIn(ofrAmt, inLmt, /* roundUp */ false);
stpAmt.out = ofrAmt.out;
// Round up for MPT output so the offer owner pays the full
// ceil(amount × rate) fee, matching direct Payment semantics. IOU uses
// floating-point arithmetic so the floor/ceil distinction is sub-epsilon
// there; preserve the historical false to avoid changing IOU behavior.
ownerGives = mulRatio(
ofrAmt.out,
transferRateOut,
QUALITY_ONE,
/*roundUp*/ std::is_same_v<TOut, MPTAmount>);
limitOfferIn(offer, ofrAmt, stpAmt, ownerGives, transferRateOut, inLmt);
}
}
@@ -785,29 +803,45 @@ BookStep<TIn, TOut, TDerived>::forEachOffer(
auto ofrAmt = offer.amount();
TAmounts stpAmt{ofrAmt.in, ofrAmt.out};
auto ownerGives = ofrAmt.out;
// A bid above MaximumAmount is limited to it below (see maxIn_). Gross
// up no more than the cap: the whole bid may not fit once grossed up
// even though a fill at the cap does.
bool const capInMPT = prevStep_ && maxIn_ && offer.owner() != assetIn.getIssuer();
auto const amtIn = [&](TIn const& in) { return capInMPT && *maxIn_ < in ? *maxIn_ : in; };
try
{
// All arithmetic in this block runs before the offer is consumed.
// A crafted MPTokensV2 offer can overflow while transfer rates or
// crossing limits are applied; remove that unusable offer instead
// of letting it persist as a tecINTERNAL source.
stpAmt.in = mulRatio(ofrAmt.in, ofrInRate, QUALITY_ONE, /*roundUp*/ true);
stpAmt.in = mulRatio(amtIn(ofrAmt.in), ofrInRate, QUALITY_ONE, /*roundUp*/ true);
// owner pays the transfer fee.
ownerGives = mulRatio(
ofrAmt.out,
ofrOutRate,
QUALITY_ONE,
/*roundUp*/ std::is_same_v<TOut, MPTAmount>);
// owner pays the transfer fee. An MPT gross-up that doesn't fit in
// an MPT amount exceeds any owner's funds (an MPT issuer's funds
// are what it can still issue), so a large, partially funded offer
// is limited to its funds below instead of removed as an overflow.
std::optional<TOut> grossOut;
if constexpr (std::is_same_v<TOut, MPTAmount>)
{
grossOut = tryMulRatio(ofrAmt.out, ofrOutRate, QUALITY_ONE, /*roundUp*/ true);
}
else
{
grossOut = mulRatio(ofrAmt.out, ofrOutRate, QUALITY_ONE, /*roundUp*/ false);
}
if (grossOut)
ownerGives = *grossOut;
auto const funds = offer.isFunded()
? ownerGives // Offer owner is issuer; they have unlimited funds
? ownerGives // Offer owner is IOU issuer; they have unlimited funds
: offers.ownerFunds();
// Only if CLOB offer
if (funds < ownerGives)
// Only a CLOB offer: an AMM offer is funded and its output rate is
// parity, so its gross-up always fits.
if (!grossOut || funds < ownerGives)
{
// We already know offer.owner()!=offer.issueOut().account
// The owner isn't an IOU issuer (those are funded). An MPT
// issuer can be here: its funds are what it can still issue.
ownerGives = funds;
stpAmt.out = mulRatio(ownerGives, QUALITY_ONE, ofrOutRate, /*roundUp*/ false);
@@ -816,32 +850,85 @@ BookStep<TIn, TOut, TDerived>::forEachOffer(
// transaction outcomes, so it must be made under an amendment.
ofrAmt = offer.limitOut(ofrAmt, stpAmt.out, /*roundUp*/ false);
stpAmt.in = mulRatio(ofrAmt.in, ofrInRate, QUALITY_ONE, /*roundUp*/ true);
}
stpAmt.in = mulRatio(amtIn(ofrAmt.in), ofrInRate, QUALITY_ONE, /*roundUp*/ true);
// Limit offer's input if MPT, BookStep is the first step (an issuer
// is making a cross-currency payment), and this offer is not owned
// by the issuer. Otherwise, OutstandingAmount may overflow.
auto const& issuer = assetIn.getIssuer();
if (isAssetInMPT && !prevStep_ && offer.owner() != issuer)
{
// Funds available to issue
auto const available = toAmount<TIn>(accountFunds(
sb,
issuer,
assetIn, // STAmount{0}, but the default is not used
FreezeHandling::IgnoreFreeze,
AuthHandling::IgnoreAuth,
j_));
if (stpAmt.in > available)
// Fee clip can floor in or out to 0. OfferStream is
// fee-blind, so it misses both: out==0 blocks the book;
// in==0 / out>0 consumes ownerGives for free. MPT at
// ordinary sizes; IOU only at the smallest out — V2
// because that IOU outcome changes.
if (sb.rules().enabled(featureMPTokensV2) &&
(stpAmt.out <= beast::kZero || ofrAmt.in <= beast::kZero))
{
limitStepIn(
offer, ofrAmt, stpAmt, ownerGives, ofrInRate, ofrOutRate, available);
removeOffer(
"Removing offer whose fee-clipped amount rounds to "
"zero");
return true;
}
// out = floor(funds / rate), so out × rate <= funds and
// ceil(out × rate) <= funds since funds is an integer. Charge
// that instead of funds; charging all of funds would burn the
// fraction that can't be delivered. IOU keeps funds; the
// difference is sub-epsilon there.
if constexpr (std::is_same_v<TOut, MPTAmount>)
{
ownerGives = mulRatio(stpAmt.out, ofrOutRate, QUALITY_ONE, /*roundUp*/ true);
}
}
// Limit offer's input if MPT and the offer is not owned by the
// issuer (an issuer's send to itself is a no-op).
bool inLimited = false;
std::optional<TIn> limitIn;
auto const& issuer = assetIn.getIssuer();
if (isAssetInMPT && offer.owner() != issuer)
{
if (!prevStep_)
{
// BookStep is the first step (an issuer is making a
// cross-currency payment). Limit to the funds available to
// issue, otherwise OutstandingAmount may overflow. This
// limits the taker, not the offer: its remainder is still
// funded by the owner, so the callback keeps it. With
// nothing left to issue there is nothing to consume, so
// stop before touching the offer.
auto const available = toAmount<TIn>(accountFunds(
sb,
issuer,
assetIn, // STAmount{0}, but the default is not used
FreezeHandling::IgnoreFreeze,
AuthHandling::IgnoreAuth,
j_));
if (available <= beast::kZero)
return false;
if (stpAmt.in > available)
{
inLimited = true;
limitIn = available;
}
}
else if (maxIn_ && ofrAmt.in > *maxIn_)
{
// Limit to MaximumAmount (see maxIn_). The remainder is
// still funded, so the callback keeps the offer instead of
// consuming it. stpAmt.in is already the grossed-up cap.
inLimited = true;
limitOfferIn(offer, ofrAmt, stpAmt, ownerGives, ofrOutRate, *maxIn_);
}
}
if (limitIn)
limitStepIn(offer, ofrAmt, stpAmt, ownerGives, ofrInRate, ofrOutRate, *limitIn);
// amtIn() grossed up no more than the cap, so the offer must have
// been limited to it above or the taker pays for less than is
// consumed.
XRPL_ASSERT(
!capInMPT || ofrAmt.in <= *maxIn_,
"xrpl::BookStep::forEachOffer : capped offer input is limited");
offerAttempted = true;
return callback(offer, ofrAmt, stpAmt, ownerGives, ofrInRate, ofrOutRate);
return callback(offer, ofrAmt, stpAmt, ownerGives, ofrInRate, ofrOutRate, inLimited);
}
catch (std::overflow_error const&)
{
@@ -876,7 +963,7 @@ BookStep<TIn, TOut, TDerived>::forEachOffer(
// If offer crossing then use either LOB quality or nullopt
// to prevent AMM being blocked by a lower quality LOB.
auto const qualityThreshold = [&] -> std::optional<Quality> {
if (sb.rules().enabled(fixAMMv1_1) && lobQuality)
if (lobQuality)
return static_cast<TDerived const*>(this)->qualityThreshold(*lobQuality);
return lobQuality;
}();
@@ -1006,7 +1093,7 @@ BookStep<TIn, TOut, TDerived>::tip(ReadView const& view) const
// as the result a LOB offer is partially crossed, and it might take a few
// iterations to fully cross the offer.
auto const qualityThreshold = [&] -> std::optional<Quality> {
if (view.rules().enabled(fixAMMv1_1) && lobQuality)
if (lobQuality)
return static_cast<TDerived const*>(this)->qualityThreshold(*lobQuality);
return std::nullopt;
}();
@@ -1094,7 +1181,8 @@ BookStep<TIn, TOut, TDerived>::revImp(
TAmounts<TIn, TOut> const& stpAmt,
TOut const& ownerGives,
std::uint32_t transferRateIn,
std::uint32_t transferRateOut) mutable -> bool {
std::uint32_t transferRateOut,
bool inLimited) mutable -> bool {
if (remainingOut <= beast::kZero)
return false;
@@ -1106,8 +1194,11 @@ BookStep<TIn, TOut, TDerived>::revImp(
remainingOut = out - result.out;
this->consumeOffer(sb, offer, ofrAmt, stpAmt, ownerGives);
// return true b/c even if the payment is satisfied,
// we need to consume the offer
return true;
// we need to consume the offer. An offer limited by what the
// issuer can issue is not consumed, though: its remainder is still
// funded, and no more of that MPT can flow through this step
// anyway.
return !inLimited;
}
auto ofrAdjAmt = ofrAmt;
@@ -1210,7 +1301,8 @@ BookStep<TIn, TOut, TDerived>::fwdImp(
TAmounts<TIn, TOut> const& stpAmt,
TOut const& ownerGives,
std::uint32_t transferRateIn,
std::uint32_t transferRateOut) mutable -> bool {
std::uint32_t transferRateOut,
bool inLimited) mutable -> bool {
XRPL_ASSERT(cache_, "xrpl::BookStep::fwdImp::eachOffer : cache is set");
if (remainingIn <= beast::kZero)
@@ -1240,8 +1332,10 @@ BookStep<TIn, TOut, TDerived>::fwdImp(
savedInsAdj.insert(stpAmt.in);
lastOut = savedOutsAdj.insert(stpAmt.out);
resultAdj = TAmounts<TIn, TOut>(sum(savedInsAdj), sum(savedOutsAdj));
// consume the offer even if stepAmt.in == remainingIn
processMore = true;
// consume the offer even if stepAmt.in == remainingIn, unless it
// was limited by what the issuer can issue: its remainder is
// still funded and must stay on the book.
processMore = !inLimited;
}
else
{
+4 -4
View File
@@ -356,10 +356,10 @@ TOfferStreamBase<TIn, TOut>::step()
offer_ = TOffer<TIn, TOut>{};
continue;
}
// The strict reduction only overflows for a crafted MPT offer, and
// MPT offers require featureMPTokensV2 (enforced at OfferCreate
// preflight). So the amendment is always enabled here and this
// legacy re-throw is unreachable in practice.
// The strict reduction only throws std::overflow_error for a
// crafted MPT offer, and MPT offers require featureMPTokensV2
// (enforced at OfferCreate preflight). So the amendment is always
// enabled here and this legacy re-throw is unreachable in practice.
// LCOV_EXCL_START
XRPL_ASSERT(
view_.rules().enabled(featureMPTokensV2),
@@ -354,11 +354,8 @@ AMMWithdraw::applyGuts(Sandbox& sb)
// Due to rounding, the LPTokenBalance of the last LP
// might not match the LP's trustline balance
if (sb.rules().enabled(fixAMMv1_1))
{
if (auto const res = verifyAndAdjustLPTokenBalance(sb, lpTokens, ammSle, accountID_); !res)
return {res.error(), false};
}
if (auto const res = verifyAndAdjustLPTokenBalance(sb, lpTokens, ammSle, accountID_); !res)
return {res.error(), false}; // LCOV_EXCL_LINE
auto const tfee = getTradingFee(ctx_.view(), *ammSle, accountID_);
@@ -591,7 +588,7 @@ AMMWithdraw::withdraw(
// Should not happen since the only LP on last withdraw
// has the balance set to the lp token trustline balance.
if (view.rules().enabled(fixAMMv1_1) && lpTokensWithdrawActual > lpTokensAMMBalance)
if (lpTokensWithdrawActual > lpTokensAMMBalance)
{
// LCOV_EXCL_START
JLOG(journal.debug()) << "AMM Withdraw: failed to withdraw, unexpected LP tokens: "
+57 -10
View File
@@ -24,6 +24,7 @@
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/Keylet.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/MPTAmount.h>
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/Quality.h>
@@ -50,6 +51,7 @@
#include <functional>
#include <memory>
#include <optional>
#include <stdexcept>
#include <tuple>
#include <utility>
@@ -413,19 +415,47 @@ OfferCreate::flowCross(
// offer taker. Set sendMax to allow for the gateway's cut.
Rate gatewayXferRate{QUALITY_ONE};
STAmount sendMax = takerAmount.in;
// Payment flow code compares quality after the transfer rate is
// included, so the threshold is computed from the grossed-up input.
STAmount thresholdIn = sendMax;
if (!sendMax.native() && (accountID_ != sendMax.getIssuer()))
{
gatewayXferRate = transferRate(psb, sendMax);
if (gatewayXferRate.value != QUALITY_ONE)
{
sendMax =
multiplyRound(takerAmount.in, gatewayXferRate, takerAmount.in.asset(), true);
try
{
sendMax = multiplyRound(
takerAmount.in, gatewayXferRate, takerAmount.in.asset(), true);
thresholdIn = sendMax;
}
catch (std::overflow_error const&)
{
// Only an MPT amount can overflow here (IOU rescales and
// XRP has no transfer rate), and MPT offers require
// featureMPTokensV2 at preflight, so this legacy re-throw
// is unreachable in practice.
// LCOV_EXCL_START
if (!takerAmount.in.holds<MPTIssue>() ||
!psb.rules().enabled(featureMPTokensV2))
throw;
// LCOV_EXCL_STOP
// The grossed-up MPT amount exceeds 2^63-1. The taker can't
// send more than its balance anyway. The threshold only
// needs the ratio, so compute it as an IOU, which can't
// overflow.
sendMax = inStartBalance;
thresholdIn = multiplyRound(
STAmount{noIssue(), takerAmount.in.mantissa()},
gatewayXferRate,
noIssue(),
true);
}
}
}
// Payment flow code compares quality after the transfer rate is
// included. Since transfer rate is incorporated compute threshold.
Quality threshold{takerAmount.out, sendMax};
Quality threshold{takerAmount.out, thresholdIn};
// If we're creating a passive offer adjust the threshold so we only
// cross offers that have a better quality than this one.
@@ -870,15 +900,32 @@ OfferCreate::applyGuts(Sandbox& sb, Sandbox& sbCancel)
return {tesSUCCESS, true};
}
// A taker redeeming an MPT into this offer pays the transfer fee, and
// BookStep grosses up TakerPays by the rate before anything else. If that
// doesn't fit in an MPT amount, every fee-paying taker overflows and
// BookStep removes the offer on the first attempt (the issuer pays no fee,
// but its send is bounded by what it can still issue). Conservative for a
// bid above MaximumAmount, which BookStep grosses up only to the cap.
// Checked on the remainder: it may fit even if the original amount didn't.
bool cannotRest = false;
if (mptV2 && saTakerPays.holds<MPTIssue>())
{
auto const rate = transferRate(sb, saTakerPays);
cannotRest = rate != kParityRate &&
!tryMulRatio(saTakerPays.mpt(), rate.value, QUALITY_ONE, /*roundUp*/ true);
}
// The remainder rests at uRate, the original pre-crossing rate. A zero
// rate (quality not representable) puts it in the directory whose index
// equals getBookBase(book), and BookTip scans keys strictly greater, so it
// could never be crossed while holding the owner's reserve. Don't place
// it; anything that crossed is kept, and a fully crossed offer has already
// returned above. Gated to preserve pre-amendment behavior.
if (mptV2 && uRate == 0)
// could never be crossed while holding the owner's reserve. Likewise, a
// bid whose fee-grossed TakerPays doesn't fit would be removed by BookStep
// on the first take. Don't place it; anything that crossed is kept, and a
// fully crossed offer has already returned above. Gated to preserve
// pre-amendment behavior.
if ((mptV2 && uRate == 0) || cannotRest)
{
JLOG(j_.debug()) << "Unrepresentable quality: remainder not placed";
JLOG(j_.debug()) << "Unrepresentable quality or fee: remainder not placed";
if (!crossed)
return {tecKILLED, false};
return {tesSUCCESS, true};
@@ -8,6 +8,7 @@
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/helpers/LendingHelpers.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/ledger/helpers/VaultHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Asset.h>
#include <xrpl/protocol/Concepts.h>
@@ -158,7 +159,7 @@ determineAsset(
std::expected<STAmount, TER>
determineClawAmount(
SLE const& sleBroker,
SLE::ConstRef sleBroker,
Asset const& vaultAsset,
std::optional<STAmount> const& amount,
SLE::ConstRef vaultSle,
@@ -169,17 +170,19 @@ determineClawAmount(
if (rules.enabled(fixCleanup3_2_0))
{
return minimumBrokerCover(
sleBroker[sfDebtTotal], TenthBips32(sleBroker[sfCoverRateMinimum]), vaultSle);
sleBroker->at(sfDebtTotal),
TenthBips32(sleBroker->at(sfCoverRateMinimum)),
vaultSle);
}
// Always round the minimum required up
NumberRoundModeGuard const mg(Number::RoundingMode::Upward);
return tenthBipsOfValue(
sleBroker[sfDebtTotal], TenthBips32(sleBroker[sfCoverRateMinimum]));
sleBroker->at(sfDebtTotal), TenthBips32(sleBroker->at(sfCoverRateMinimum)));
}();
// The subtraction probably won't round, but round down if it does.
NumberRoundModeGuard const mg(Number::RoundingMode::Downward);
return sleBroker[sfCoverAvailable] - minRequiredCover;
return sleBroker->at(sfCoverAvailable) - minRequiredCover;
}();
if (maxClawAmount <= beast::kZero)
return std::unexpected(tecINSUFFICIENT_FUNDS);
@@ -187,12 +190,23 @@ determineClawAmount(
// Use the vaultAsset here, because it will be the right type in all
// circumstances. The amount may be an IOU indicating the pseudo-account's
// asset, which is correct, but not what is needed here.
if (!amount || *amount == beast::kZero)
return STAmount{vaultAsset, maxClawAmount};
Number const magnitude{*amount};
if (magnitude > maxClawAmount)
return STAmount{vaultAsset, maxClawAmount};
return STAmount{vaultAsset, magnitude};
STAmount requested = [&] {
if (!amount || *amount == beast::kZero)
return STAmount{vaultAsset, maxClawAmount};
Number const magnitude{*amount};
if (magnitude > maxClawAmount)
return STAmount{vaultAsset, maxClawAmount};
return STAmount{vaultAsset, magnitude};
}();
if (getVaultVersion(vaultSle) != VaultVersion::FixedPrecision)
return requested;
STAmount rounded = debitToPosteriorBrokerCoverScale(
vaultSle, sleBroker, requested, Number::RoundingMode::TowardsZero);
if (rounded == beast::kZero)
return std::unexpected(tecPRECISION_LOSS);
return rounded;
}
template <ValidIssueType T>
@@ -294,17 +308,25 @@ LoanBrokerCoverClawback::preclaim(PreclaimContext const& ctx)
}
auto const findClawAmount =
determineClawAmount(*sleBroker, vaultAsset, amount, vault, ctx.view.rules());
determineClawAmount(sleBroker, vaultAsset, amount, vault, ctx.view.rules());
if (!findClawAmount)
{
JLOG(ctx.j.warn()) << "LoanBroker cover is already at minimum.";
JLOG(ctx.j.warn()) << "LoanBroker cover is already at minimum, or the claw amount "
"rounds to zero: "
<< transToken(findClawAmount.error());
return findClawAmount.error();
}
STAmount const& clawAmount = *findClawAmount;
if (auto const ret = canApplyToBrokerCover(
ctx.view, sleBroker, vaultAsset, clawAmount, ctx.j, "LoanBrokerCoverClawback"))
return ret;
// FixedPrecision outflows already rounded at the posterior exponent; the
// live CoverAvailable scale used by canApplyToBrokerCover would reject a
// re-fining clawback as sub-ULP.
if (getVaultVersion(vault) != VaultVersion::FixedPrecision)
{
if (auto const ret = canApplyToBrokerCover(
ctx.view, sleBroker, vaultAsset, clawAmount, ctx.j, "LoanBrokerCoverClawback"))
return ret;
}
// Explicitly check the balance of the trust line / MPT to make sure the
// balance is actually there. It should always match `sfCoverAvailable`, so
@@ -357,7 +379,7 @@ LoanBrokerCoverClawback::doApply()
auto const vaultAsset = vault->at(sfAsset);
auto const findClawAmount =
determineClawAmount(*sleBroker, vaultAsset, amount, vault, view().rules());
determineClawAmount(sleBroker, vaultAsset, amount, vault, view().rules());
if (!findClawAmount)
return tecINTERNAL; // LCOV_EXCL_LINE
STAmount const& clawAmount = *findClawAmount;
@@ -5,8 +5,10 @@
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/ledger/helpers/LendingHelpers.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/ledger/helpers/VaultHelpers.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STLedgerEntry.h>
@@ -104,22 +106,35 @@ LoanBrokerCoverDeposit::preclaim(PreclaimContext const& ctx)
// here in preclaim lets us reject sub-cover-scale dust early with tecPRECISION_LOSS instead of
// failing only in doApply.
auto const roundedAmount = [&] -> STAmount {
if (getVaultVersion(vault) == VaultVersion::FixedPrecision)
{
// Cover deposit is an inflow to CoverAvailable, so it must floor
// the sum, not just the delta.
return creditToPosteriorBrokerCoverScale(
vault, sleBroker, amount, Number::RoundingMode::Downward);
}
if (!fix320Enabled)
return tx[sfAmount];
return STAmount{amount};
return roundToScale(
tx[sfAmount],
amount,
scale(sleBroker->at(sfCoverAvailable), vaultAsset),
Number::RoundingMode::Downward);
}();
if (fix320Enabled && roundedAmount == beast::kZero)
// fixCleanup3_2_0 is enabled on the network, so this also covers the
// FixedPrecision zero-credit case.
if (fix320Enabled && roundedAmount <= beast::kZero)
{
JLOG(ctx.j.warn()) << "LoanBrokerCoverDeposit: deposit amount: " << tx[sfAmount]
<< " is zero at loan broker scale";
JLOG(ctx.j.warn()) << "LoanBrokerCoverDeposit: deposit amount: " << amount
<< " is zero or negative at loan broker scale";
return tecPRECISION_LOSS;
}
if (auto const ter = checkOptionalBrokerCoverInflow(vault, sleBroker, roundedAmount);
!isTesSuccess(ter))
return ter;
if (accountHolds(
ctx.view,
account,
@@ -155,6 +170,13 @@ LoanBrokerCoverDeposit::doApply()
// see the rationale comment in preclaim.
bool const fix320Enabled = view().rules().enabled(fixCleanup3_2_0);
auto const amount = [&] -> STAmount {
if (getVaultVersion(vault) == VaultVersion::FixedPrecision)
{
// Cover deposit is an inflow to CoverAvailable, so it must floor
// the sum, not just the delta.
return creditToPosteriorBrokerCoverScale(
vault, broker, tx[sfAmount], Number::RoundingMode::Downward);
}
if (!fix320Enabled)
return tx[sfAmount];
@@ -164,12 +186,12 @@ LoanBrokerCoverDeposit::doApply()
Number::RoundingMode::Downward);
}();
// We validated zero-amount in preclaim, if we ended up with zero now, fail hard.
if (amount == beast::kZero)
// We validated zero-or-negative amount in preclaim, if we ended up with one now, fail hard.
if (amount <= beast::kZero)
{
// LCOV_EXCL_START
JLOG(j_.error()) << "LoanBrokerCoverDeposit: deposit amount: " << tx[sfAmount]
<< " is zero";
<< " is zero or negative";
return tecINTERNAL;
// LCOV_EXCL_STOP
}
@@ -8,6 +8,7 @@
#include <xrpl/ledger/helpers/CredentialHelpers.h>
#include <xrpl/ledger/helpers/LendingHelpers.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/ledger/helpers/VaultHelpers.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Protocol.h>
@@ -103,10 +104,28 @@ LoanBrokerCoverWithdraw::preclaim(PreclaimContext const& ctx)
if (amount.asset() != vaultAsset)
return tecWRONG_ASSET;
// Helper handles both IOU and MPT correctly without explicit branching.
if (auto const ret = canApplyToBrokerCover(
ctx.view, sleBroker, vaultAsset, amount, ctx.j, "LoanBrokerCoverWithdraw"))
return ret;
STAmount roundedAmount{amount};
if (getVaultVersion(vault) == VaultVersion::FixedPrecision)
{
// FixedPrecision outflows are rounded at the posterior exponent here
// instead of going through canApplyToBrokerCover: the live
// CoverAvailable scale it uses would reject a re-fining withdrawal as
// sub-ULP.
roundedAmount = debitToPosteriorBrokerCoverScale(
vault, sleBroker, amount, Number::RoundingMode::TowardsZero);
if (roundedAmount == beast::kZero)
{
JLOG(ctx.j.warn()) << "LoanBrokerCoverWithdraw: withdraw amount: " << amount
<< " is zero at loan broker scale";
return tecPRECISION_LOSS;
}
}
else
{
if (auto const ret = canApplyToBrokerCover(
ctx.view, sleBroker, vaultAsset, roundedAmount, ctx.j, "LoanBrokerCoverWithdraw"))
return ret;
}
// The broker's pseudo-account is the source of funds.
auto const pseudoAccountID = sleBroker->at(sfAccount);
@@ -129,7 +148,15 @@ LoanBrokerCoverWithdraw::preclaim(PreclaimContext const& ctx)
AuthType authType = AuthType::WeakAuth;
if (account != dstAcct)
{
if (auto const ret = canWithdraw(ctx.view, tx))
// Check the amount actually sent: on FixedPrecision it is the rounded
// amount, not sfAmount.
if (auto const ret = canWithdraw(
ctx.view,
account,
dstAcct,
roundedAmount,
tx.isFieldPresent(sfDestinationTag),
tx[~sfCredentialIDs]))
return ret;
// The destination account must have consented to receive the asset by
@@ -184,9 +211,9 @@ LoanBrokerCoverWithdraw::preclaim(PreclaimContext const& ctx)
tenthBipsOfValue(currentDebtTotal, TenthBips32(sleBroker->at(sfCoverRateMinimum))),
scale(currentDebtTotal, vaultAsset));
}();
if (coverAvail < amount)
if (coverAvail < roundedAmount)
return tecINSUFFICIENT_FUNDS;
if ((coverAvail - amount) < minimumCover)
if ((coverAvail - roundedAmount) < minimumCover)
return tecINSUFFICIENT_FUNDS;
auto const freezeHandling = fix330Enabled && dstAcct == vaultAsset.getIssuer()
@@ -199,7 +226,7 @@ LoanBrokerCoverWithdraw::preclaim(PreclaimContext const& ctx)
vaultAsset,
freezeHandling,
AuthHandling::ZeroIfUnauthorized,
ctx.j) < amount)
ctx.j) < roundedAmount)
return tecINSUFFICIENT_FUNDS;
return tesSUCCESS;
@@ -211,7 +238,7 @@ LoanBrokerCoverWithdraw::doApply()
auto const& tx = ctx_.tx;
auto const brokerID = tx[sfLoanBrokerID];
auto const amount = tx[sfAmount];
auto const requestedAmount = tx[sfAmount];
auto const dstAcct = tx[~sfDestination].value_or(accountID_);
auto const broker = view().peek(keylet::loanBroker(brokerID));
@@ -223,6 +250,10 @@ LoanBrokerCoverWithdraw::doApply()
return tecINTERNAL; // LCOV_EXCL_LINE
auto const vaultAsset = vault->at(sfAsset);
auto const amount = getVaultVersion(vault) == VaultVersion::FixedPrecision
? debitToPosteriorBrokerCoverScale(
vault, broker, requestedAmount, Number::RoundingMode::TowardsZero)
: requestedAmount;
auto const brokerPseudoID = *broker->at(sfAccount);
@@ -239,6 +270,7 @@ LoanBrokerCoverWithdraw::doApply()
brokerPseudoID,
preFeeBalance_,
amount,
amount,
j_);
}
@@ -6,6 +6,7 @@
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/LendingHelpers.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/ledger/helpers/VaultHelpers.h>
#include <xrpl/protocol/Asset.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
@@ -77,7 +78,7 @@ LoanBrokerDelete::preclaim(PreclaimContext const& ctx)
{
// Any remaining debt should have been wiped out by the last Loan
// Delete. This check is purely defensive.
auto const scale = getAssetsTotalScale(vault);
auto const scale = getVaultScale(vault);
auto const rounded =
roundToAsset(asset, debtTotal, scale, Number::RoundingMode::TowardsZero);
@@ -94,21 +95,38 @@ LoanBrokerDelete::preclaim(PreclaimContext const& ctx)
auto const coverAvailable = STAmount{asset, sleBroker->at(sfCoverAvailable)};
// If there are assets in the cover, broker will receive them on deletion.
// So we need to check if the broker owner is deep frozen for that asset.
if (coverAvailable > beast::kZero)
{
auto const brokerPseudo = sleBroker->at(sfAccount);
// Pre-fixCleanup3_5_0: only freeze checks apply to the cover payout.
// Post-fixCleanup3_5_0: apply the cover-withdraw transfer and authorization checks too.
if (ctx.view.rules().enabled(fixCleanup3_5_0))
{
// Like a cover withdrawal, the payout skips the lsfMPTCanTransfer check so that an
// issuer cannot trap the broker's cover.
if (auto const ret = canTransfer(
ctx.view, asset, brokerPseudo, brokerOwner, WaiveMPTCanTransfer::Yes))
return ret;
if (auto const ret = requireAuth(ctx.view, asset, brokerOwner, AuthType::WeakAuth))
return ret;
if (!holdingExists(ctx.view, brokerOwner, asset))
{
if (auto const ret = canAddHolding(ctx.view, asset); !isTesSuccess(ret))
return ret;
}
}
if (auto const ret = checkDeepFrozen(ctx.view, brokerOwner, asset))
{
JLOG(ctx.j.warn()) << "Broker owner account is frozen.";
return ret;
}
}
if (ctx.view.rules().enabled(fixCleanup3_2_0))
{
if (coverAvailable > beast::kZero)
if (ctx.view.rules().enabled(fixCleanup3_2_0))
{
auto const brokerPseudo = sleBroker->at(sfAccount);
if (auto const ret = checkFrozen(ctx.view, brokerPseudo, asset))
{
JLOG(ctx.j.warn()) << "Broker pseudo-account is frozen/locked.";
@@ -151,8 +169,36 @@ LoanBrokerDelete::doApply()
return tefBAD_LEDGER; // LCOV_EXCL_LINE
}
auto const owner = view().peek(keylet::account(accountID_));
if (!owner)
return tefBAD_LEDGER; // LCOV_EXCL_LINE
// Pre-fixCleanup3_5_0: the owner count drops after the pseudo-account is erased.
// Post-fixCleanup3_5_0: it drops first, so the owner's holding created below can use the
// reserve that the broker frees.
bool const fix350Enabled = view().rules().enabled(fixCleanup3_5_0);
if (fix350Enabled)
{
// Decreases the owner count by two: one for the LoanBroker object, and one
// for the pseudo-account.
decreaseOwnerCountForObject(view(), owner, broker, 2, j_);
}
{
auto const coverAvailable = STAmount{vaultAsset, broker->at(sfCoverAvailable)};
// Pre-fixCleanup3_5_0: the cover goes straight to accountSend, so an MPT payout to an
// owner with no MPToken fails with tecNO_AUTH.
// Post-fixCleanup3_5_0: a nonzero cover first gets the owner a holding if it has none, as a
// withdrawal to self does.
if (fix350Enabled && coverAvailable > beast::kZero &&
!holdingExists(view(), accountID_, vaultAsset))
{
if (auto const ter = addEmptyHolding(
ctx_.getApplyViewContext(), accountID_, preFeeBalance_, vaultAsset, j_))
return ter;
}
if (auto const ter = accountSend(
view(), brokerPseudoID, accountID_, coverAvailable, j_, {}, WaiveTransferFee::Yes))
return ter;
@@ -185,15 +231,8 @@ LoanBrokerDelete::doApply()
view().erase(brokerPseudoSLE);
{
auto const owner = view().peek(keylet::account(accountID_));
if (!owner)
return tefBAD_LEDGER; // LCOV_EXCL_LINE
// Decreases the owner count by two: one for the LoanBroker object, and
// one for the pseudo-account.
if (!fix350Enabled)
decreaseOwnerCountForObject(view(), owner, broker, 2, j_);
}
view().erase(broker);
@@ -232,7 +232,9 @@ LoanBrokerSet::preclaim(PreclaimContext const& ctx)
{
if (auto const value = tx[field]; value &&
((fix350Enabled && !isRepresentable(asset, *value)) ||
STAmount{asset, *value} != *value))
STAmount{asset, *value} != *value ||
(getVaultVersion(sleVault) == VaultVersion::FixedPrecision &&
!isOnVaultBaseGrid(asset, *value, getVaultBaseScale(sleVault)))))
{
JLOG(ctx.j.warn()) << field.f->getName() << " (" << *value
<< ") can not be represented as a(n) " << to_string(asset) << ".";
@@ -6,6 +6,7 @@
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/LendingHelpers.h>
#include <xrpl/ledger/helpers/VaultHelpers.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h> // IWYU pragma: keep
@@ -120,7 +121,7 @@ LoanDelete::doApply()
roundToAsset(
vaultSle->at(sfAsset),
debtTotalProxy,
getAssetsTotalScale(vaultSle),
getVaultScale(vaultSle),
Number::RoundingMode::TowardsZero) == beast::kZero,
"xrpl::LoanDelete::doApply",
"last loan, remaining debt rounds to zero");
Loaded 100 of 245 files, more files were not shown because too many files have changed in this diff. Show more