From 88794a1ea9b22af5bc7591cb79d1a1b632a153b2 Mon Sep 17 00:00:00 2001 From: Denis Angell Date: Thu, 14 May 2026 10:20:15 +0200 Subject: [PATCH] docs: add Doxygen comments across xrpl and xrpld Bulk documentation pass covering 702 C++ source files in src/libxrpl, src/xrpld, and include/xrpl. Adds class, function, parameter, and invariant docs per docs/DOCUMENTATION_STANDARDS.md. Squashed from the original three-part series (part 1 / part 2 / part 3) to avoid merge-conflict noise when rebasing the work onto current develop. --- include/xrpl/basics/FileUtilities.h | 36 + include/xrpl/crypto/RFC1751.h | 173 +++- include/xrpl/crypto/csprng.h | 136 ++- include/xrpl/crypto/secure_erase.h | 43 +- include/xrpl/ledger/AcceptedLedgerTx.h | 91 +- include/xrpl/ledger/AmendmentTable.h | 276 +++++- include/xrpl/ledger/ApplyView.h | 569 ++++++----- include/xrpl/ledger/ApplyViewImpl.h | 113 ++- include/xrpl/ledger/BookDirs.h | 109 +++ include/xrpl/ledger/BookListeners.h | 64 +- include/xrpl/ledger/CachedSLEs.h | 36 + include/xrpl/ledger/CachedView.h | 115 ++- include/xrpl/ledger/CanonicalTXSet.h | 140 ++- include/xrpl/ledger/Dir.h | 139 ++- include/xrpl/ledger/Ledger.h | 534 +++++++++-- include/xrpl/ledger/LedgerTiming.h | 184 ++-- include/xrpl/ledger/OpenView.h | 259 +++-- include/xrpl/ledger/OrderBookDB.h | 173 ++-- include/xrpl/ledger/PaymentSandbox.h | 354 +++++-- include/xrpl/ledger/PendingSaves.h | 146 ++- include/xrpl/ledger/RawView.h | 124 ++- include/xrpl/ledger/ReadView.h | 323 +++++-- include/xrpl/ledger/Sandbox.h | 73 +- include/xrpl/ledger/View.h | 298 ++++-- include/xrpl/ledger/detail/ApplyStateTable.h | 269 +++++- include/xrpl/ledger/detail/ApplyViewBase.h | 198 ++++ include/xrpl/ledger/detail/RawStateTable.h | 184 +++- include/xrpl/ledger/detail/ReadViewFwdRange.h | 170 +++- include/xrpl/ledger/helpers/AMMHelpers.h | 773 ++++++++++----- .../xrpl/ledger/helpers/AccountRootHelpers.h | 211 ++++- .../xrpl/ledger/helpers/CredentialHelpers.h | 226 ++++- include/xrpl/ledger/helpers/DelegateHelpers.h | 77 +- .../xrpl/ledger/helpers/DirectoryHelpers.h | 224 ++++- include/xrpl/ledger/helpers/EscrowHelpers.h | 169 +++- include/xrpl/ledger/helpers/LendingHelpers.h | 772 ++++++++++++--- include/xrpl/ledger/helpers/MPTokenHelpers.h | 409 +++++++- include/xrpl/ledger/helpers/NFTokenHelpers.h | 313 +++++- include/xrpl/ledger/helpers/OfferHelpers.h | 44 +- .../ledger/helpers/PaymentChannelHelpers.h | 29 + .../ledger/helpers/PermissionedDEXHelpers.h | 81 +- .../xrpl/ledger/helpers/RippleStateHelpers.h | 440 +++++++-- include/xrpl/ledger/helpers/TokenHelpers.h | 605 ++++++++++-- include/xrpl/ledger/helpers/VaultHelpers.h | 160 +++- include/xrpl/nodestore/Backend.h | 221 +++-- include/xrpl/nodestore/Database.h | 416 +++++--- include/xrpl/nodestore/DatabaseRotating.h | 76 +- include/xrpl/nodestore/DummyScheduler.h | 50 +- include/xrpl/nodestore/Factory.h | 78 +- include/xrpl/nodestore/Manager.h | 141 ++- include/xrpl/nodestore/NodeObject.h | 124 ++- include/xrpl/nodestore/Scheduler.h | 115 ++- include/xrpl/nodestore/Task.h | 46 +- include/xrpl/nodestore/Types.h | 66 +- include/xrpl/nodestore/detail/BatchWriter.h | 130 ++- .../xrpl/nodestore/detail/DatabaseNodeImp.h | 150 ++- .../nodestore/detail/DatabaseRotatingImp.h | 152 ++- include/xrpl/nodestore/detail/DecodedBlob.h | 80 +- include/xrpl/nodestore/detail/EncodedBlob.h | 118 ++- include/xrpl/nodestore/detail/ManagerImp.h | 59 ++ include/xrpl/nodestore/detail/codec.h | 160 +++- include/xrpl/nodestore/detail/varint.h | 118 ++- include/xrpl/protocol/AMMCore.h | 185 +++- include/xrpl/protocol/AccountID.h | 174 +++- include/xrpl/protocol/AmountConversions.h | 221 +++++ include/xrpl/protocol/ApiVersion.h | 182 +++- include/xrpl/protocol/Asset.h | 325 ++++++- include/xrpl/protocol/Batch.h | 41 + include/xrpl/protocol/Book.h | 185 +++- include/xrpl/protocol/BuildInfo.h | 159 +++- include/xrpl/protocol/Concepts.h | 122 ++- include/xrpl/protocol/ErrorCodes.h | 427 ++++++--- include/xrpl/protocol/Feature.h | 293 +++++- include/xrpl/protocol/Fees.h | 77 +- include/xrpl/protocol/HashPrefix.h | 137 ++- include/xrpl/protocol/IOUAmount.h | 214 ++++- include/xrpl/protocol/Indexes.h | 652 +++++++++++-- include/xrpl/protocol/InnerObjectFormats.h | 54 +- include/xrpl/protocol/Issue.h | 209 ++++- include/xrpl/protocol/KeyType.h | 63 +- include/xrpl/protocol/Keylet.h | 66 +- include/xrpl/protocol/KnownFormats.h | 194 +++- include/xrpl/protocol/LedgerFormats.h | 202 ++-- include/xrpl/protocol/LedgerHeader.h | 164 +++- include/xrpl/protocol/LedgerShortcut.h | 51 +- include/xrpl/protocol/MPTAmount.h | 170 +++- include/xrpl/protocol/MPTIssue.h | 203 +++- include/xrpl/protocol/MultiApiJson.h | 207 +++- .../xrpl/protocol/NFTSyntheticSerializer.h | 60 +- include/xrpl/protocol/NFTokenID.h | 101 +- include/xrpl/protocol/NFTokenOfferID.h | 81 +- include/xrpl/protocol/PathAsset.h | 139 ++- include/xrpl/protocol/PayChan.h | 38 + include/xrpl/protocol/Permissions.h | 175 +++- include/xrpl/protocol/Protocol.h | 450 ++++++--- include/xrpl/protocol/PublicKey.h | 277 ++++-- include/xrpl/protocol/Quality.h | 370 ++++++-- include/xrpl/protocol/QualityFunction.h | 145 ++- include/xrpl/protocol/RPCErr.h | 38 +- include/xrpl/protocol/Rate.h | 136 ++- include/xrpl/protocol/RippleLedgerHash.h | 22 + include/xrpl/protocol/Rules.h | 123 ++- include/xrpl/protocol/SField.h | 393 ++++++-- include/xrpl/protocol/SOTemplate.h | 192 +++- include/xrpl/protocol/STAccount.h | 153 ++- include/xrpl/protocol/STAmount.h | 582 +++++++++++- include/xrpl/protocol/STArray.h | 252 +++++ include/xrpl/protocol/STBase.h | 212 ++++- include/xrpl/protocol/STBitString.h | 145 ++- include/xrpl/protocol/STBlob.h | 130 ++- include/xrpl/protocol/STCurrency.h | 146 +++ include/xrpl/protocol/STExchange.h | 183 +++- include/xrpl/protocol/STInteger.h | 146 +++ include/xrpl/protocol/STIssue.h | 129 +++ include/xrpl/protocol/STLedgerEntry.h | 157 +++- include/xrpl/protocol/STNumber.h | 159 +++- include/xrpl/protocol/STObject.h | 459 ++++++++- include/xrpl/protocol/STParsedJSON.h | 61 +- include/xrpl/protocol/STPathSet.h | 324 ++++++- include/xrpl/protocol/STTakesAsset.h | 66 +- include/xrpl/protocol/STTx.h | 347 ++++++- include/xrpl/protocol/STValidation.h | 266 +++++- include/xrpl/protocol/STVector256.h | 187 +++- include/xrpl/protocol/STXChainBridge.h | 190 ++++ include/xrpl/protocol/SecretKey.h | 214 ++++- include/xrpl/protocol/Seed.h | 157 +++- include/xrpl/protocol/SeqProxy.h | 149 ++- include/xrpl/protocol/Serializer.h | 522 ++++++++++- include/xrpl/protocol/Sign.h | 141 ++- include/xrpl/protocol/SystemParameters.h | 141 ++- include/xrpl/protocol/TER.h | 441 ++++++--- include/xrpl/protocol/TxFlags.h | 291 ++++-- include/xrpl/protocol/TxFormats.h | 116 ++- include/xrpl/protocol/TxMeta.h | 198 +++- include/xrpl/protocol/TxSearched.h | 43 +- include/xrpl/protocol/UintTypes.h | 216 ++++- include/xrpl/protocol/Units.h | 492 +++++++++- include/xrpl/protocol/XChainAttestations.h | 633 ++++++++++++- include/xrpl/protocol/XRPAmount.h | 158 +++- include/xrpl/protocol/detail/STVar.h | 201 +++- include/xrpl/protocol/detail/b58_utils.h | 146 ++- include/xrpl/protocol/detail/secp256k1.h | 37 + include/xrpl/protocol/detail/token_errors.h | 106 ++- include/xrpl/protocol/digest.h | 211 ++++- include/xrpl/protocol/json_get_or_throw.h | 155 ++- include/xrpl/protocol/jss.h | 90 +- include/xrpl/protocol/messages.h | 33 +- include/xrpl/protocol/nft.h | 138 ++- include/xrpl/protocol/nftPageMask.h | 31 +- include/xrpl/protocol/serialize.h | 31 +- include/xrpl/protocol/st.h | 33 + include/xrpl/protocol/tokens.h | 293 +++++- include/xrpl/shamap/Family.h | 88 +- include/xrpl/shamap/FullBelowCache.h | 171 +++- include/xrpl/shamap/SHAMap.h | 621 +++++++++--- .../xrpl/shamap/SHAMapAccountStateLeafNode.h | 87 +- include/xrpl/shamap/SHAMapAddNode.h | 101 +- include/xrpl/shamap/SHAMapInnerNode.h | 323 ++++++- include/xrpl/shamap/SHAMapItem.h | 125 ++- include/xrpl/shamap/SHAMapLeafNode.h | 96 +- include/xrpl/shamap/SHAMapMissingNode.h | 70 +- include/xrpl/shamap/SHAMapNodeID.h | 127 ++- include/xrpl/shamap/SHAMapSyncFilter.h | 67 +- include/xrpl/shamap/SHAMapTreeNode.h | 305 ++++-- include/xrpl/shamap/SHAMapTxLeafNode.h | 91 +- .../xrpl/shamap/SHAMapTxPlusMetaLeafNode.h | 106 ++- include/xrpl/shamap/TreeNodeCache.h | 56 +- include/xrpl/shamap/detail/TaggedPointer.h | 330 ++++--- include/xrpl/tx/ApplyContext.h | 180 +++- include/xrpl/tx/SignerEntries.h | 80 +- include/xrpl/tx/Transactor.h | 697 ++++++++++++-- include/xrpl/tx/apply.h | 211 +++-- include/xrpl/tx/applySteps.h | 487 +++++++--- include/xrpl/tx/invariants/AMMInvariant.h | 196 ++++ include/xrpl/tx/invariants/FreezeInvariant.h | 180 +++- include/xrpl/tx/invariants/InvariantCheck.h | 420 +++++++-- .../tx/invariants/InvariantCheckPrivilege.h | 178 +++- .../xrpl/tx/invariants/LoanBrokerInvariant.h | 125 ++- include/xrpl/tx/invariants/LoanInvariant.h | 75 +- include/xrpl/tx/invariants/MPTInvariant.h | 168 +++- include/xrpl/tx/invariants/NFTInvariant.h | 143 ++- .../tx/invariants/PermissionedDEXInvariant.h | 64 ++ .../invariants/PermissionedDomainInvariant.h | 116 ++- include/xrpl/tx/invariants/VaultInvariant.h | 164 +++- include/xrpl/tx/paths/AMMLiquidity.h | 163 +++- include/xrpl/tx/paths/AMMOffer.h | 265 +++++- include/xrpl/tx/paths/BookTip.h | 73 +- include/xrpl/tx/paths/Flow.h | 94 +- include/xrpl/tx/paths/Offer.h | 222 ++++- include/xrpl/tx/paths/OfferStream.h | 223 ++++- include/xrpl/tx/paths/RippleCalc.h | 167 +++- include/xrpl/tx/paths/detail/AmountSpec.h | 39 + include/xrpl/tx/paths/detail/EitherAmount.h | 69 ++ include/xrpl/tx/paths/detail/FlatSets.h | 40 +- include/xrpl/tx/paths/detail/FlowDebugInfo.h | 263 +++++- include/xrpl/tx/paths/detail/StepChecks.h | 75 ++ include/xrpl/tx/paths/detail/Steps.h | 770 ++++++++++----- include/xrpl/tx/paths/detail/StrandFlow.h | 454 ++++++--- .../tx/transactors/account/AccountDelete.h | 126 +++ .../xrpl/tx/transactors/account/AccountSet.h | 149 +++ .../tx/transactors/account/SetRegularKey.h | 75 ++ .../tx/transactors/account/SignerListSet.h | 156 ++- .../xrpl/tx/transactors/bridge/XChainBridge.h | 503 ++++++++-- .../xrpl/tx/transactors/check/CheckCancel.h | 90 ++ include/xrpl/tx/transactors/check/CheckCash.h | 144 +++ .../xrpl/tx/transactors/check/CheckCreate.h | 139 +++ .../credentials/CredentialAccept.h | 74 ++ .../credentials/CredentialCreate.h | 84 ++ .../credentials/CredentialDelete.h | 72 ++ .../tx/transactors/delegate/DelegateSet.h | 83 +- include/xrpl/tx/transactors/dex/AMMBid.h | 124 ++- include/xrpl/tx/transactors/dex/AMMClawback.h | 152 ++- include/xrpl/tx/transactors/dex/AMMContext.h | 115 ++- include/xrpl/tx/transactors/dex/AMMCreate.h | 120 ++- include/xrpl/tx/transactors/dex/AMMDelete.h | 78 +- include/xrpl/tx/transactors/dex/AMMDeposit.h | 361 ++++--- include/xrpl/tx/transactors/dex/AMMVote.h | 106 ++- include/xrpl/tx/transactors/dex/AMMWithdraw.h | 459 ++++++--- include/xrpl/tx/transactors/dex/OfferCancel.h | 63 ++ include/xrpl/tx/transactors/dex/OfferCreate.h | 184 +++- include/xrpl/tx/transactors/did/DIDDelete.h | 73 ++ include/xrpl/tx/transactors/did/DIDSet.h | 50 + .../xrpl/tx/transactors/escrow/EscrowCancel.h | 97 ++ .../xrpl/tx/transactors/escrow/EscrowCreate.h | 144 +++ .../xrpl/tx/transactors/escrow/EscrowFinish.h | 180 ++++ .../lending/LoanBrokerCoverClawback.h | 82 ++ .../lending/LoanBrokerCoverDeposit.h | 81 ++ .../lending/LoanBrokerCoverWithdraw.h | 98 ++ .../tx/transactors/lending/LoanBrokerDelete.h | 95 ++ .../tx/transactors/lending/LoanBrokerSet.h | 135 +++ .../xrpl/tx/transactors/lending/LoanDelete.h | 91 ++ .../xrpl/tx/transactors/lending/LoanManage.h | 170 +++- include/xrpl/tx/transactors/lending/LoanPay.h | 188 ++++ include/xrpl/tx/transactors/lending/LoanSet.h | 203 ++++ .../tx/transactors/nft/NFTokenAcceptOffer.h | 171 ++++ include/xrpl/tx/transactors/nft/NFTokenBurn.h | 69 ++ .../tx/transactors/nft/NFTokenCancelOffer.h | 72 ++ .../tx/transactors/nft/NFTokenCreateOffer.h | 92 ++ include/xrpl/tx/transactors/nft/NFTokenMint.h | 159 +++- .../xrpl/tx/transactors/nft/NFTokenModify.h | 85 ++ .../xrpl/tx/transactors/oracle/OracleDelete.h | 84 +- .../xrpl/tx/transactors/oracle/OracleSet.h | 103 +- .../tx/transactors/payment/DepositPreauth.h | 89 +- include/xrpl/tx/transactors/payment/Payment.h | 167 +++- .../payment_channel/PaymentChannelClaim.h | 137 +++ .../payment_channel/PaymentChannelCreate.h | 115 +++ .../payment_channel/PaymentChannelFund.h | 79 ++ .../PermissionedDomainDelete.h | 93 +- .../PermissionedDomainSet.h | 92 +- include/xrpl/tx/transactors/system/Batch.h | 162 ++++ include/xrpl/tx/transactors/system/Change.h | 95 ++ .../tx/transactors/system/LedgerStateFix.h | 101 ++ .../xrpl/tx/transactors/system/TicketCreate.h | 143 ++- include/xrpl/tx/transactors/token/Clawback.h | 78 ++ .../tx/transactors/token/MPTokenAuthorize.h | 120 +++ .../transactors/token/MPTokenIssuanceCreate.h | 157 ++++ .../token/MPTokenIssuanceDestroy.h | 81 ++ .../tx/transactors/token/MPTokenIssuanceSet.h | 149 +++ include/xrpl/tx/transactors/token/TrustSet.h | 173 ++++ .../xrpl/tx/transactors/vault/VaultClawback.h | 113 +++ .../xrpl/tx/transactors/vault/VaultCreate.h | 124 +++ .../xrpl/tx/transactors/vault/VaultDelete.h | 93 ++ .../xrpl/tx/transactors/vault/VaultDeposit.h | 114 +++ include/xrpl/tx/transactors/vault/VaultSet.h | 109 +++ .../xrpl/tx/transactors/vault/VaultWithdraw.h | 120 +++ src/libxrpl/basics/Archive.cpp | 38 +- src/libxrpl/basics/BasicConfig.cpp | 176 +++- src/libxrpl/basics/CountedObject.cpp | 33 + src/libxrpl/basics/FileUtilities.cpp | 8 + src/libxrpl/basics/Log.cpp | 191 +++- src/libxrpl/basics/MallocTrim.cpp | 60 +- src/libxrpl/basics/Number.cpp | 398 ++++++-- src/libxrpl/basics/ResolverAsio.cpp | 298 +++++- src/libxrpl/basics/StringUtilities.cpp | 122 ++- src/libxrpl/basics/UptimeClock.cpp | 44 +- src/libxrpl/basics/base64.cpp | 120 ++- src/libxrpl/basics/contract.cpp | 39 + src/libxrpl/basics/make_SSLContext.cpp | 152 ++- src/libxrpl/basics/mulDiv.cpp | 34 + .../beast/clock/basic_seconds_clock.cpp | 71 +- src/libxrpl/beast/core/CurrentThreadName.cpp | 80 +- src/libxrpl/beast/core/SemanticVersion.cpp | 184 ++-- src/libxrpl/beast/insight/Collector.cpp | 15 + src/libxrpl/beast/insight/Groups.cpp | 80 ++ src/libxrpl/beast/insight/Hook.cpp | 12 + src/libxrpl/beast/insight/Metric.cpp | 18 + src/libxrpl/beast/insight/NullCollector.cpp | 72 ++ src/libxrpl/beast/insight/StatsDCollector.cpp | 297 +++++- src/libxrpl/beast/net/IPAddressConversion.cpp | 15 + src/libxrpl/beast/net/IPAddressV4.cpp | 43 + src/libxrpl/beast/net/IPAddressV6.cpp | 42 + src/libxrpl/beast/net/IPEndpoint.cpp | 94 +- src/libxrpl/beast/utility/beast_Journal.cpp | 103 +- .../beast/utility/beast_PropertyStream.cpp | 378 +++++++- src/libxrpl/crypto/RFC1751.cpp | 190 +++- src/libxrpl/crypto/csprng.cpp | 27 +- src/libxrpl/crypto/secure_erase.cpp | 19 + src/libxrpl/json/JsonPropertyStream.cpp | 76 +- src/libxrpl/json/Output.cpp | 48 + src/libxrpl/json/Writer.cpp | 202 ++++ src/libxrpl/json/json_reader.cpp | 337 ++++++- src/libxrpl/json/json_value.cpp | 478 +++++++++- src/libxrpl/json/json_valueiterator.cpp | 147 ++- src/libxrpl/json/json_writer.cpp | 295 +++++- src/libxrpl/json/to_string.cpp | 36 + src/libxrpl/ledger/AcceptedLedgerTx.cpp | 15 +- src/libxrpl/ledger/ApplyStateTable.cpp | 50 +- src/libxrpl/ledger/ApplyView.cpp | 169 ++++ src/libxrpl/ledger/ApplyViewBase.cpp | 22 + src/libxrpl/ledger/ApplyViewImpl.cpp | 16 + src/libxrpl/ledger/BookDirs.cpp | 104 ++ src/libxrpl/ledger/BookListeners.cpp | 19 +- src/libxrpl/ledger/CachedView.cpp | 19 +- src/libxrpl/ledger/CanonicalTXSet.cpp | 31 +- src/libxrpl/ledger/Dir.cpp | 78 ++ src/libxrpl/ledger/Ledger.cpp | 218 ++++- src/libxrpl/ledger/OpenView.cpp | 29 + src/libxrpl/ledger/PaymentSandbox.cpp | 260 ++++- src/libxrpl/ledger/RawStateTable.cpp | 215 ++++- src/libxrpl/ledger/ReadView.cpp | 28 + src/libxrpl/ledger/View.cpp | 55 +- src/libxrpl/ledger/helpers/AMMHelpers.cpp | 266 ++++-- .../ledger/helpers/AccountRootHelpers.cpp | 133 ++- .../ledger/helpers/CredentialHelpers.cpp | 242 ++++- .../ledger/helpers/DirectoryHelpers.cpp | 111 ++- src/libxrpl/ledger/helpers/LendingHelpers.cpp | 671 +++++++++---- src/libxrpl/ledger/helpers/MPTokenHelpers.cpp | 457 +++++++-- src/libxrpl/ledger/helpers/NFTokenHelpers.cpp | 308 +++--- src/libxrpl/ledger/helpers/OfferHelpers.cpp | 12 + .../ledger/helpers/PaymentChannelHelpers.cpp | 12 +- .../ledger/helpers/PermissionedDEXHelpers.cpp | 49 +- .../ledger/helpers/RippleStateHelpers.cpp | 191 ++-- src/libxrpl/ledger/helpers/TokenHelpers.cpp | 446 +++++++-- src/libxrpl/ledger/helpers/VaultHelpers.cpp | 39 +- src/libxrpl/nodestore/BatchWriter.cpp | 72 +- src/libxrpl/nodestore/Database.cpp | 131 ++- src/libxrpl/nodestore/DatabaseNodeImp.cpp | 90 +- src/libxrpl/nodestore/DatabaseRotatingImp.cpp | 31 +- src/libxrpl/nodestore/DecodedBlob.cpp | 67 +- src/libxrpl/nodestore/DummyScheduler.cpp | 9 +- src/libxrpl/nodestore/ManagerImp.cpp | 97 +- src/libxrpl/nodestore/NodeObject.cpp | 23 +- .../nodestore/backend/MemoryFactory.cpp | 156 +++ src/libxrpl/nodestore/backend/NuDBFactory.cpp | 311 +++++- src/libxrpl/nodestore/backend/NullFactory.cpp | 79 +- .../nodestore/backend/RocksDBFactory.cpp | 252 ++++- src/libxrpl/protocol/AMMCore.cpp | 113 +++ src/libxrpl/protocol/AccountID.cpp | 137 ++- src/libxrpl/protocol/Asset.cpp | 97 ++ src/libxrpl/protocol/Book.cpp | 49 + src/libxrpl/protocol/BuildInfo.cpp | 134 ++- src/libxrpl/protocol/ErrorCodes.cpp | 147 ++- src/libxrpl/protocol/Feature.cpp | 287 +++++- src/libxrpl/protocol/IOUAmount.cpp | 208 +++- src/libxrpl/protocol/Indexes.cpp | 579 ++++++++++-- src/libxrpl/protocol/InnerObjectFormats.cpp | 46 +- src/libxrpl/protocol/Issue.cpp | 116 +++ src/libxrpl/protocol/Keylet.cpp | 25 + src/libxrpl/protocol/LedgerFormats.cpp | 68 ++ src/libxrpl/protocol/LedgerHeader.cpp | 87 +- src/libxrpl/protocol/MPTAmount.cpp | 71 ++ src/libxrpl/protocol/MPTIssue.cpp | 90 +- .../protocol/NFTSyntheticSerializer.cpp | 41 + src/libxrpl/protocol/NFTokenID.cpp | 108 ++- src/libxrpl/protocol/NFTokenOfferID.cpp | 72 ++ src/libxrpl/protocol/PathAsset.cpp | 27 + src/libxrpl/protocol/Permissions.cpp | 142 ++- src/libxrpl/protocol/Protocol.cpp | 56 ++ src/libxrpl/protocol/PublicKey.cpp | 198 +++- src/libxrpl/protocol/Quality.cpp | 168 +++- src/libxrpl/protocol/QualityFunction.cpp | 64 ++ src/libxrpl/protocol/RPCErr.cpp | 33 +- src/libxrpl/protocol/Rate2.cpp | 118 +++ src/libxrpl/protocol/Rules.cpp | 134 ++- src/libxrpl/protocol/SField.cpp | 118 ++- src/libxrpl/protocol/SOTemplate.cpp | 84 +- src/libxrpl/protocol/STAccount.cpp | 112 ++- src/libxrpl/protocol/STAmount.cpp | 605 +++++++++--- src/libxrpl/protocol/STArray.cpp | 166 ++++ src/libxrpl/protocol/STBase.cpp | 47 +- src/libxrpl/protocol/STBlob.cpp | 86 ++ src/libxrpl/protocol/STCurrency.cpp | 114 +++ src/libxrpl/protocol/STInteger.cpp | 141 ++- src/libxrpl/protocol/STIssue.cpp | 101 +- src/libxrpl/protocol/STLedgerEntry.cpp | 125 +++ src/libxrpl/protocol/STNumber.cpp | 129 ++- src/libxrpl/protocol/STObject.cpp | 517 +++++++++- src/libxrpl/protocol/STParsedJSON.cpp | 313 +++++- src/libxrpl/protocol/STPathSet.cpp | 175 +++- src/libxrpl/protocol/STTakesAsset.cpp | 30 +- src/libxrpl/protocol/STTx.cpp | 411 +++++++- src/libxrpl/protocol/STValidation.cpp | 117 ++- src/libxrpl/protocol/STVar.cpp | 125 +++ src/libxrpl/protocol/STVector256.cpp | 83 ++ src/libxrpl/protocol/STXChainBridge.cpp | 48 + src/libxrpl/protocol/SecretKey.cpp | 211 ++++- src/libxrpl/protocol/Seed.cpp | 104 ++ src/libxrpl/protocol/Serializer.cpp | 312 +++++- src/libxrpl/protocol/Sign.cpp | 84 +- src/libxrpl/protocol/TER.cpp | 70 +- src/libxrpl/protocol/TxFormats.cpp | 69 +- src/libxrpl/protocol/TxMeta.cpp | 121 ++- src/libxrpl/protocol/UintTypes.cpp | 102 +- src/libxrpl/protocol/XChainAttestations.cpp | 311 +++++- src/libxrpl/protocol/digest.cpp | 23 + src/libxrpl/protocol/tokens.cpp | 288 +++++- src/libxrpl/rdb/DatabaseCon.cpp | 115 ++- src/libxrpl/rdb/SociDB.cpp | 231 ++++- src/libxrpl/shamap/SHAMap.cpp | 606 ++++++++++-- src/libxrpl/shamap/SHAMapDelta.cpp | 73 +- src/libxrpl/shamap/SHAMapInnerNode.cpp | 221 +++++ src/libxrpl/shamap/SHAMapLeafNode.cpp | 50 + src/libxrpl/shamap/SHAMapNodeID.cpp | 30 +- src/libxrpl/shamap/SHAMapSync.cpp | 160 ++-- src/libxrpl/shamap/SHAMapTreeNode.cpp | 35 +- src/libxrpl/tx/ApplyContext.cpp | 125 ++- src/libxrpl/tx/SignerEntries.cpp | 9 +- src/libxrpl/tx/Transactor.cpp | 661 +++++++++---- src/libxrpl/tx/apply.cpp | 100 +- src/libxrpl/tx/applySteps.cpp | 217 ++++- src/libxrpl/tx/invariants/AMMInvariant.cpp | 36 +- src/libxrpl/tx/invariants/FreezeInvariant.cpp | 219 ++++- src/libxrpl/tx/invariants/InvariantCheck.cpp | 31 +- .../tx/invariants/LoanBrokerInvariant.cpp | 73 +- src/libxrpl/tx/invariants/LoanInvariant.cpp | 15 +- src/libxrpl/tx/invariants/MPTInvariant.cpp | 117 ++- src/libxrpl/tx/invariants/NFTInvariant.cpp | 125 ++- .../invariants/PermissionedDEXInvariant.cpp | 12 +- .../PermissionedDomainInvariant.cpp | 65 +- src/libxrpl/tx/invariants/VaultInvariant.cpp | 127 +++ src/libxrpl/tx/paths/AMMLiquidity.cpp | 52 +- src/libxrpl/tx/paths/AMMOffer.cpp | 141 ++- src/libxrpl/tx/paths/BookStep.cpp | 727 +++++++++----- src/libxrpl/tx/paths/BookTip.cpp | 60 +- src/libxrpl/tx/paths/DirectStep.cpp | 426 +++++++-- src/libxrpl/tx/paths/Flow.cpp | 29 + src/libxrpl/tx/paths/MPTEndpointStep.cpp | 315 ++++++- src/libxrpl/tx/paths/OfferStream.cpp | 98 +- src/libxrpl/tx/paths/PaySteps.cpp | 94 +- src/libxrpl/tx/paths/RippleCalc.cpp | 75 +- src/libxrpl/tx/paths/XRPEndpointStep.cpp | 210 ++++- .../tx/transactors/account/AccountDelete.cpp | 123 ++- .../tx/transactors/account/AccountSet.cpp | 36 +- .../tx/transactors/account/SetRegularKey.cpp | 60 +- .../tx/transactors/account/SignerListSet.cpp | 116 ++- .../tx/transactors/bridge/XChainBridge.cpp | 596 ++++++++++-- .../tx/transactors/check/CheckCancel.cpp | 69 +- .../tx/transactors/check/CheckCash.cpp | 142 ++- .../tx/transactors/check/CheckCreate.cpp | 167 +++- .../credentials/CredentialAccept.cpp | 16 +- .../credentials/CredentialCreate.cpp | 27 +- .../credentials/CredentialDelete.cpp | 18 +- .../tx/transactors/delegate/DelegateSet.cpp | 22 +- .../tx/transactors/delegate/DelegateUtils.cpp | 13 + src/libxrpl/tx/transactors/dex/AMMBid.cpp | 107 ++- .../tx/transactors/dex/AMMClawback.cpp | 70 +- src/libxrpl/tx/transactors/dex/AMMCreate.cpp | 88 +- src/libxrpl/tx/transactors/dex/AMMDelete.cpp | 21 +- src/libxrpl/tx/transactors/dex/AMMDeposit.cpp | 202 ++-- src/libxrpl/tx/transactors/dex/AMMVote.cpp | 68 +- .../tx/transactors/dex/AMMWithdraw.cpp | 66 +- .../tx/transactors/dex/OfferCancel.cpp | 12 +- .../tx/transactors/dex/OfferCreate.cpp | 212 ++--- src/libxrpl/tx/transactors/did/DIDDelete.cpp | 11 +- src/libxrpl/tx/transactors/did/DIDSet.cpp | 126 ++- src/libxrpl/tx/transactors/escrow/Escrow.cpp | 23 + .../tx/transactors/escrow/EscrowCancel.cpp | 64 +- .../tx/transactors/escrow/EscrowCreate.cpp | 283 ++++-- .../tx/transactors/escrow/EscrowFinish.cpp | 86 +- .../lending/LoanBrokerCoverClawback.cpp | 118 ++- .../lending/LoanBrokerCoverDeposit.cpp | 20 +- .../lending/LoanBrokerCoverWithdraw.cpp | 91 +- .../transactors/lending/LoanBrokerDelete.cpp | 14 +- .../tx/transactors/lending/LoanBrokerSet.cpp | 15 +- .../tx/transactors/lending/LoanDelete.cpp | 39 +- .../tx/transactors/lending/LoanManage.cpp | 103 +- .../tx/transactors/lending/LoanPay.cpp | 87 ++ .../tx/transactors/lending/LoanSet.cpp | 29 +- .../tx/transactors/nft/NFTokenAcceptOffer.cpp | 155 +-- .../tx/transactors/nft/NFTokenBurn.cpp | 25 +- .../tx/transactors/nft/NFTokenCancelOffer.cpp | 19 +- .../tx/transactors/nft/NFTokenCreateOffer.cpp | 22 +- .../tx/transactors/nft/NFTokenMint.cpp | 115 +-- .../tx/transactors/nft/NFTokenModify.cpp | 11 +- .../tx/transactors/oracle/OracleDelete.cpp | 52 +- .../tx/transactors/oracle/OracleSet.cpp | 85 +- .../tx/transactors/payment/DepositPreauth.cpp | 17 +- .../tx/transactors/payment/Payment.cpp | 168 ++-- .../payment_channel/PaymentChannelClaim.cpp | 15 +- .../payment_channel/PaymentChannelCreate.cpp | 83 +- .../payment_channel/PaymentChannelFund.cpp | 79 +- .../PermissionedDomainDelete.cpp | 59 +- .../PermissionedDomainSet.cpp | 37 +- src/libxrpl/tx/transactors/system/Batch.cpp | 168 ++-- src/libxrpl/tx/transactors/system/Change.cpp | 43 +- .../tx/transactors/system/LedgerStateFix.cpp | 31 +- .../tx/transactors/system/TicketCreate.cpp | 122 ++- src/libxrpl/tx/transactors/token/Clawback.cpp | 226 ++++- .../tx/transactors/token/MPTokenAuthorize.cpp | 136 ++- .../token/MPTokenIssuanceCreate.cpp | 150 ++- .../token/MPTokenIssuanceDestroy.cpp | 98 +- .../transactors/token/MPTokenIssuanceSet.cpp | 172 +++- src/libxrpl/tx/transactors/token/TrustSet.cpp | 164 +++- .../tx/transactors/vault/VaultClawback.cpp | 150 ++- .../tx/transactors/vault/VaultCreate.cpp | 129 ++- .../tx/transactors/vault/VaultDelete.cpp | 70 ++ .../tx/transactors/vault/VaultDeposit.cpp | 114 ++- src/libxrpl/tx/transactors/vault/VaultSet.cpp | 82 +- .../tx/transactors/vault/VaultWithdraw.cpp | 147 ++- src/xrpld/consensus/Consensus.cpp | 78 +- src/xrpld/consensus/Consensus.h | 332 +++++-- src/xrpld/consensus/ConsensusParms.h | 305 ++++-- src/xrpld/consensus/ConsensusProposal.h | 242 +++-- src/xrpld/consensus/ConsensusTypes.h | 291 ++++-- src/xrpld/consensus/DisputedTx.h | 250 +++-- src/xrpld/consensus/LedgerTrie.h | 276 +++++- src/xrpld/consensus/Validations.h | 725 +++++++++----- src/xrpld/overlay/Cluster.h | 112 ++- src/xrpld/overlay/ClusterNode.h | 67 ++ src/xrpld/overlay/Compression.h | 103 +- src/xrpld/overlay/Message.h | 175 +++- src/xrpld/overlay/Overlay.h | 295 ++++-- src/xrpld/overlay/Peer.h | 242 ++++- src/xrpld/overlay/PeerSet.h | 124 ++- src/xrpld/overlay/ReduceRelayCommon.h | 133 ++- src/xrpld/overlay/Slot.h | 505 +++++++--- src/xrpld/overlay/Squelch.h | 93 +- src/xrpld/overlay/detail/Cluster.cpp | 24 + src/xrpld/overlay/detail/ConnectAttempt.cpp | 266 +++++- src/xrpld/overlay/detail/ConnectAttempt.h | 399 +++++--- src/xrpld/overlay/detail/Handshake.cpp | 233 ++++- src/xrpld/overlay/detail/Handshake.h | 320 +++++-- src/xrpld/overlay/detail/Message.cpp | 58 +- src/xrpld/overlay/detail/OverlayImpl.cpp | 514 ++++++++-- src/xrpld/overlay/detail/OverlayImpl.h | 541 +++++++++-- src/xrpld/overlay/detail/PeerImp.cpp | 887 +++++++++++++++++- src/xrpld/overlay/detail/PeerImp.h | 734 +++++++++++---- .../overlay/detail/PeerReservationTable.cpp | 94 +- src/xrpld/overlay/detail/PeerSet.cpp | 87 +- src/xrpld/overlay/detail/ProtocolMessage.h | 228 +++-- src/xrpld/overlay/detail/ProtocolVersion.cpp | 112 ++- src/xrpld/overlay/detail/ProtocolVersion.h | 120 ++- src/xrpld/overlay/detail/TrafficCount.cpp | 67 ++ src/xrpld/overlay/detail/TrafficCount.h | 288 +++--- src/xrpld/overlay/detail/Tuning.h | 122 ++- src/xrpld/overlay/detail/TxMetrics.cpp | 60 ++ src/xrpld/overlay/detail/TxMetrics.h | 261 +++++- src/xrpld/overlay/detail/ZeroCopyStream.h | 180 +++- src/xrpld/overlay/make_Overlay.h | 71 +- src/xrpld/overlay/predicates.h | 171 +++- src/xrpld/peerfinder/PeerfinderManager.h | 457 +++++++-- src/xrpld/peerfinder/Slot.h | 106 ++- src/xrpld/peerfinder/detail/Bootcache.cpp | 112 ++- src/xrpld/peerfinder/detail/Bootcache.h | 175 +++- src/xrpld/peerfinder/detail/Checker.h | 119 ++- src/xrpld/peerfinder/detail/Counts.h | 259 ++++- src/xrpld/peerfinder/detail/Endpoint.cpp | 20 + src/xrpld/peerfinder/detail/Fixed.h | 62 +- src/xrpld/peerfinder/detail/Handouts.h | 171 +++- src/xrpld/peerfinder/detail/Livecache.h | 219 ++++- src/xrpld/peerfinder/detail/Logic.h | 530 ++++++++++- .../peerfinder/detail/PeerfinderConfig.cpp | 44 +- .../peerfinder/detail/PeerfinderManager.cpp | 138 +++ src/xrpld/peerfinder/detail/SlotImp.cpp | 142 ++- src/xrpld/peerfinder/detail/SlotImp.h | 230 ++++- src/xrpld/peerfinder/detail/Source.h | 56 +- src/xrpld/peerfinder/detail/SourceStrings.cpp | 64 ++ src/xrpld/peerfinder/detail/SourceStrings.h | 32 +- src/xrpld/peerfinder/detail/Store.h | 58 +- src/xrpld/peerfinder/detail/StoreSqdb.h | 86 +- src/xrpld/peerfinder/detail/Tuning.h | 188 ++-- src/xrpld/peerfinder/detail/iosformat.h | 199 +++- src/xrpld/peerfinder/make_Manager.h | 36 +- src/xrpld/rpc/BookChanges.h | 102 +- src/xrpld/rpc/CTID.h | 100 +- src/xrpld/rpc/Context.h | 98 +- src/xrpld/rpc/DeliveredAmount.h | 128 ++- src/xrpld/rpc/GRPCHandlers.h | 87 +- src/xrpld/rpc/MPTokenIssuanceID.h | 71 +- src/xrpld/rpc/Output.h | 36 + src/xrpld/rpc/RPCCall.h | 114 ++- src/xrpld/rpc/RPCHandler.h | 74 +- src/xrpld/rpc/RPCSub.h | 64 +- src/xrpld/rpc/Role.h | 129 ++- src/xrpld/rpc/ServerHandler.h | 257 ++++- src/xrpld/rpc/Status.h | 148 ++- src/xrpld/rpc/detail/AccountAssets.cpp | 34 +- src/xrpld/rpc/detail/AccountAssets.h | 81 ++ src/xrpld/rpc/detail/AssetCache.cpp | 96 +- src/xrpld/rpc/detail/AssetCache.h | 118 ++- src/xrpld/rpc/detail/DeliveredAmount.cpp | 91 +- src/xrpld/rpc/detail/Handler.cpp | 161 +++- src/xrpld/rpc/detail/Handler.h | 128 ++- src/xrpld/rpc/detail/LegacyPathFind.cpp | 20 + src/xrpld/rpc/detail/LegacyPathFind.h | 53 ++ src/xrpld/rpc/detail/MPT.h | 93 +- src/xrpld/rpc/detail/MPTokenIssuanceID.cpp | 19 +- src/xrpld/rpc/detail/PathRequest.cpp | 319 +++++-- src/xrpld/rpc/detail/PathRequest.h | 327 ++++++- src/xrpld/rpc/detail/PathRequestManager.cpp | 150 ++- src/xrpld/rpc/detail/PathRequestManager.h | 208 +++- src/xrpld/rpc/detail/Pathfinder.cpp | 596 +++++++++--- src/xrpld/rpc/detail/Pathfinder.h | 208 +++- src/xrpld/rpc/detail/PathfinderUtils.h | 71 ++ src/xrpld/rpc/detail/RPCCall.cpp | 274 +++++- src/xrpld/rpc/detail/RPCHandler.cpp | 162 ++-- src/xrpld/rpc/detail/RPCHelpers.cpp | 223 ++++- src/xrpld/rpc/detail/RPCHelpers.h | 218 +++-- src/xrpld/rpc/detail/RPCLedgerHelpers.cpp | 230 ++++- src/xrpld/rpc/detail/RPCLedgerHelpers.h | 230 +++-- src/xrpld/rpc/detail/RPCSub.cpp | 100 +- src/xrpld/rpc/detail/RippleLineCache.cpp | 32 + src/xrpld/rpc/detail/RippleLineCache.h | 15 + src/xrpld/rpc/detail/Role.cpp | 151 ++- src/xrpld/rpc/detail/ServerHandler.cpp | 143 ++- src/xrpld/rpc/detail/Status.cpp | 59 +- src/xrpld/rpc/detail/TransactionSign.cpp | 517 +++++++--- src/xrpld/rpc/detail/TransactionSign.h | 206 +++- src/xrpld/rpc/detail/TrustLine.cpp | 138 ++- src/xrpld/rpc/detail/TrustLine.h | 205 +++- src/xrpld/rpc/detail/Tuning.h | 214 ++++- src/xrpld/rpc/detail/WSInfoSub.h | 75 ++ src/xrpld/rpc/handlers/ChannelVerify.cpp | 40 +- src/xrpld/rpc/handlers/Handlers.h | 856 +++++++++++++++-- src/xrpld/rpc/handlers/VaultInfo.cpp | 47 +- .../rpc/handlers/account/AccountChannels.cpp | 71 +- .../handlers/account/AccountCurrencies.cpp | 36 +- .../rpc/handlers/account/AccountInfo.cpp | 119 ++- .../rpc/handlers/account/AccountLines.cpp | 104 +- .../rpc/handlers/account/AccountNFTs.cpp | 70 +- .../rpc/handlers/account/AccountObjects.cpp | 129 ++- .../rpc/handlers/account/AccountOffers.cpp | 65 +- src/xrpld/rpc/handlers/account/AccountTx.cpp | 163 +++- .../rpc/handlers/account/GatewayBalances.cpp | 224 ++--- .../rpc/handlers/account/NoRippleCheck.cpp | 74 +- src/xrpld/rpc/handlers/account/OwnerInfo.cpp | 50 +- src/xrpld/rpc/handlers/admin/BlackList.cpp | 39 + src/xrpld/rpc/handlers/admin/UnlList.cpp | 35 + .../rpc/handlers/admin/data/CanDelete.cpp | 41 +- .../rpc/handlers/admin/data/LedgerCleaner.cpp | 28 + .../rpc/handlers/admin/data/LedgerRequest.cpp | 44 +- .../admin/keygen/ValidationCreate.cpp | 56 +- .../handlers/admin/keygen/WalletPropose.cpp | 100 +- .../rpc/handlers/admin/keygen/WalletPropose.h | 35 + src/xrpld/rpc/handlers/admin/log/LogLevel.cpp | 47 +- .../rpc/handlers/admin/log/LogRotate.cpp | 27 + src/xrpld/rpc/handlers/admin/peer/Connect.cpp | 34 +- .../admin/peer/PeerReservationsAdd.cpp | 53 +- .../admin/peer/PeerReservationsDel.cpp | 35 +- .../admin/peer/PeerReservationsList.cpp | 19 +- src/xrpld/rpc/handlers/admin/peer/Peers.cpp | 38 +- .../admin/server_control/LedgerAccept.cpp | 37 + .../handlers/admin/server_control/Stop.cpp | 25 + .../admin/signing/ChannelAuthorize.cpp | 56 +- src/xrpld/rpc/handlers/admin/signing/Sign.cpp | 35 +- .../rpc/handlers/admin/signing/SignFor.cpp | 39 +- .../handlers/admin/status/ConsensusInfo.cpp | 20 + .../rpc/handlers/admin/status/FetchInfo.cpp | 33 + .../rpc/handlers/admin/status/GetCounts.cpp | 67 +- .../rpc/handlers/admin/status/GetCounts.h | 40 + src/xrpld/rpc/handlers/admin/status/Print.cpp | 35 + .../handlers/admin/status/ValidatorInfo.cpp | 54 +- .../admin/status/ValidatorListSites.cpp | 30 + .../rpc/handlers/admin/status/Validators.cpp | 36 + src/xrpld/rpc/handlers/ledger/Ledger.cpp | 84 +- src/xrpld/rpc/handlers/ledger/Ledger.h | 108 ++- .../rpc/handlers/ledger/LedgerClosed.cpp | 28 + .../rpc/handlers/ledger/LedgerCurrent.cpp | 25 + src/xrpld/rpc/handlers/ledger/LedgerData.cpp | 81 +- src/xrpld/rpc/handlers/ledger/LedgerDiff.cpp | 24 +- src/xrpld/rpc/handlers/ledger/LedgerEntry.cpp | 486 +++++++++- .../rpc/handlers/ledger/LedgerEntryHelpers.h | 212 +++++ .../rpc/handlers/ledger/LedgerHeader.cpp | 51 +- src/xrpld/rpc/handlers/orderbook/AMMInfo.cpp | 77 +- .../rpc/handlers/orderbook/BookChanges.cpp | 32 + .../rpc/handlers/orderbook/BookOffers.cpp | 87 ++ .../handlers/orderbook/DepositAuthorized.cpp | 69 +- .../handlers/orderbook/GetAggregatePrice.cpp | 147 ++- .../rpc/handlers/orderbook/NFTBuyOffers.cpp | 33 + .../rpc/handlers/orderbook/NFTOffersHelpers.h | 67 +- .../rpc/handlers/orderbook/NFTSellOffers.cpp | 33 + src/xrpld/rpc/handlers/orderbook/PathFind.cpp | 49 + .../rpc/handlers/orderbook/RipplePathFind.cpp | 156 ++- .../rpc/handlers/server_info/Feature.cpp | 47 +- src/xrpld/rpc/handlers/server_info/Fee.cpp | 39 + .../rpc/handlers/server_info/Manifest.cpp | 44 +- .../server_info/ServerDefinitions.cpp | 144 ++- .../handlers/server_info/ServerDefinitions.h | 27 + .../rpc/handlers/server_info/ServerInfo.cpp | 32 + .../rpc/handlers/server_info/ServerState.cpp | 44 + src/xrpld/rpc/handlers/server_info/Version.h | 63 ++ .../rpc/handlers/subscribe/Subscribe.cpp | 60 +- .../rpc/handlers/subscribe/Unsubscribe.cpp | 66 +- .../rpc/handlers/transaction/Simulate.cpp | 145 +++ src/xrpld/rpc/handlers/transaction/Submit.cpp | 60 +- .../transaction/SubmitMultiSigned.cpp | 28 +- .../handlers/transaction/TransactionEntry.cpp | 58 +- src/xrpld/rpc/handlers/transaction/Tx.cpp | 216 ++++- .../rpc/handlers/transaction/TxHistory.cpp | 48 +- .../handlers/transaction/TxReduceRelay.cpp | 42 + src/xrpld/rpc/handlers/utility/Ping.cpp | 38 +- src/xrpld/rpc/handlers/utility/Random.cpp | 48 +- src/xrpld/rpc/json_body.h | 109 ++- 702 files changed, 91270 insertions(+), 15018 deletions(-) diff --git a/include/xrpl/basics/FileUtilities.h b/include/xrpl/basics/FileUtilities.h index 8cf7e4893f..25803cae1f 100644 --- a/include/xrpl/basics/FileUtilities.h +++ b/include/xrpl/basics/FileUtilities.h @@ -7,12 +7,48 @@ namespace xrpl { +/** Read the entire contents of a file into a string. + * + * Resolves `sourcePath` to its canonical (absolute, symlink-free) form before + * opening it, which prevents TOCTOU races between path resolution and the open. + * When `maxSize` is supplied and the file exceeds that byte count, `ec` is set + * to `boost::system::errc::file_too_large` and an empty string is returned + * without reading any data. + * + * All errors — non-existent path, permission denial, size exceeded, open + * failure, and mid-read I/O error — are reported through `ec`. The function + * never throws. + * + * @param ec Output error code; set on any failure, left unchanged on success. + * @param sourcePath Path to the file to read; must exist and be resolvable. + * @param maxSize Optional upper bound on file size in bytes. If the file is + * larger, `ec` is set to `errc::file_too_large` and `{}` is returned. + * @return The full file contents on success, or an empty string on any error. + * @note EOF during the single-pass read is not an error; only `bad()` (hardware + * or stream-corruption failure) triggers an error code after the read. + */ std::string getFileContents( boost::system::error_code& ec, boost::filesystem::path const& sourcePath, std::optional maxSize = std::nullopt); +/** Write a string to a file, creating or truncating it as necessary. + * + * Opens `destPath` with `std::ios::out | std::ios::trunc`, so any existing + * content is discarded and the file is created if it does not yet exist. + * This is a full replacement, not an atomic rename-and-swap; callers that + * require crash-safe writes must implement that at a higher level. + * + * All errors — open failure and mid-write I/O error — are reported through + * `ec`. The function never throws. + * + * @param ec Output error code; set on any failure, left unchanged on success. + * @param destPath Path to the destination file; parent directory must exist. + * @param contents Data to write; written in a single `<<` operation. + * @note Unlike `getFileContents`, this function does not call `canonical()` + * because the destination file may not yet exist. + */ void writeFileContents( boost::system::error_code& ec, diff --git a/include/xrpl/crypto/RFC1751.h b/include/xrpl/crypto/RFC1751.h index c99c691ba0..9870321b2d 100644 --- a/include/xrpl/crypto/RFC1751.h +++ b/include/xrpl/crypto/RFC1751.h @@ -1,3 +1,18 @@ +/** @file + * RFC 1751 mnemonic encoding for 128-bit XRPL wallet seeds. + * + * Declares the `RFC1751` utility class, which encodes and decodes 128-bit + * binary keys as sequences of short English words drawn from a 2048-word + * dictionary. Each word represents exactly 11 bits; a 64-bit block maps + * to 6 words (64 data bits + 2 parity bits = 66 bits). A full 128-bit key + * therefore encodes as 12 words in two back-to-back 6-word groups. + * + * The primary consumer is `Seed.cpp`, which uses the codec to produce + * human-readable wallet seed mnemonics. `NetworkOPs.cpp` also uses + * `getWordFromBlob` to derive a stable short label for the local node's + * public key in log output. + */ + #pragma once #include @@ -5,39 +20,183 @@ namespace xrpl { +/** XRPL adaptation of the RFC 1751 128-bit mnemonic key codec. + * + * Converts 128-bit binary keys to and from sequences of 12 English words + * using a fixed 2048-word dictionary. The dictionary is split at index 571: + * words 0–570 have 1–3 characters; words 571–2047 are all exactly 4 + * characters. This property is exploited internally to halve binary-search + * range during decoding. + * + * All methods are static; this class is a pure stateless namespace and is + * never instantiated. + * + * @note `Seed.cpp` reverses the 16 seed bytes before passing them to + * `getEnglishFromKey` and after receiving them from `getKeyFromEnglish` + * to satisfy the RFC's big-endian byte-order convention. + */ class RFC1751 { public: + /** Decode a 12-word mnemonic string into a 128-bit binary key. + * + * Splits @p strHuman on whitespace (multiple spaces are collapsed), + * validates and normalises each word via `standard()`, looks each one + * up in the dictionary, and packs the resulting 11-bit indices into two + * 8-byte binary halves. Each half carries a 2-bit parity check computed + * from the 64 data bits; the decode fails with `-2` if the recomputed + * parity does not match. + * + * @param strKey Output parameter; set to the 16-byte binary key on + * success. Unchanged on any failure return. + * @param strHuman 12 space-separated words to decode. Leading and + * trailing whitespace is trimmed before splitting. + * @return 1 success — @p strKey holds the decoded 16-byte key. + * @return 0 a word was not found in the dictionary. + * @return -1 malformed input: word count ≠ 12, or a word exceeds 4 + * characters. + * @return -2 all words are valid but the 2-bit parity check failed, + * indicating a transcription error. + * + * @note The four distinct return codes must not be collapsed; `-2` + * (parity failure) implies the words themselves were individually + * valid and is a different diagnostic than `0` (unknown word). + */ static int getKeyFromEnglish(std::string& strKey, std::string const& strHuman); + /** Encode a 128-bit binary key as 12 space-separated English words. + * + * Encodes the first 8 bytes of @p strKey as 6 words and the next 8 + * bytes as a further 6 words, then joins the two groups with a single + * space. A 2-bit parity value is appended to each 64-bit block before + * encoding to support transcription-error detection on decode. + * + * Encoding is lossless and cannot fail for valid 16-byte input; no + * return code is needed. + * + * @param strHuman Output parameter; receives the 12-word mnemonic string. + * @param strKey The 16-byte (128-bit) binary key to encode. Behaviour + * is undefined if fewer than 16 bytes are provided. + */ static void getEnglishFromKey(std::string& strHuman, std::string const& strKey); - /** Chooses a single dictionary word from the data. - - This is not particularly secure but it can be useful to provide - a unique name for something given a GUID or fixed data. We use - it to turn the pubkey_node into an easily remembered and identified - 4 character string. - */ + /** Map arbitrary binary data to a single dictionary word. + * + * Applies the Jenkins one-at-a-time hash to the input bytes, then + * indexes into the 2048-word dictionary using the hash modulo 2048. + * The result is a stable, reproducible label for the input data. + * + * @param blob Pointer to the input data. + * @param bytes Number of bytes to hash. + * @return A single uppercase dictionary word of 1–4 characters. + * + * @note This function is **not** cryptographically secure. It is + * intended only for producing human-readable identifiers, such as + * the `shroudedHostId` label derived from a node's public key in + * `NetworkOPs.cpp`. + */ static std::string getWordFromBlob(void const* blob, size_t bytes); private: + /** Read up to 11 bits from a byte array at an arbitrary bit offset. + * + * Assembles up to 3 adjacent bytes into a 24-bit window, shifts right + * to align the target field, and masks to @p length bits. Works across + * byte boundaries. The output buffer for the 66-bit block (64 data + + * 2 parity) must be at least 9 bytes. + * + * @param s Source byte array (at least ⌈(start + length) / 8⌉ + 1 + * bytes long; 9 bytes for the full 66-bit block). + * @param start First bit to read (0-based). Must be ≥ 0. + * @param length Number of bits to read. Must satisfy 0 ≤ length ≤ 11 + * and start + length ≤ 66. + * @return The extracted value, right-justified and zero-extended. + */ static unsigned long extract(char const* s, int start, int length); + + /** Encode an 8-byte binary block as 6 space-separated dictionary words. + * + * Appends a 9th byte carrying a 2-bit parity value (sum of all 32 + * two-bit pairs in the 64-bit payload, placed at bits 64–65), then + * calls `extract()` at six 11-bit offsets to obtain dictionary indices. + * + * @param strHuman Output; receives the 6-word space-separated string. + * @param strData Exactly 8 bytes of binary data to encode. + */ static void btoe(std::string& strHuman, std::string const& strData); + + /** Write up to 11 bits into a byte array at an arbitrary bit offset. + * + * ORs the bit field into the target bytes; the output buffer must be + * zero-initialised before the first call because this function + * accumulates bits with bitwise OR rather than assignment. + * + * @param s Target byte array (must be zero-initialised). + * @param x Value to insert (only the low @p length bits are used). + * @param start First destination bit (0-based). Must be ≥ 0. + * @param length Number of bits to write. Must satisfy 0 ≤ length ≤ 11 + * and start + length ≤ 66. + */ static void insert(char* s, int x, int start, int length); + + /** Normalise a mnemonic word for dictionary lookup. + * + * Applies three in-place transformations to tolerate common + * handwriting and OCR ambiguities: lowercased letters are uppercased, + * `'1'` is replaced by `'L'`, `'0'` by `'O'`, and `'5'` by `'S'`. + * + * @param strWord Word to normalise in place. + */ static void standard(std::string& strWord); + + /** Binary-search the dictionary within a given index range. + * + * The dictionary is sorted, and its first 571 entries (indices 0–570) + * are words of 1–3 characters while the remaining 1477 (indices + * 571–2047) are all exactly 4 characters. Callers restrict the range + * based on word length to halve the search space. + * + * @param strWord Word to search for (must already be normalised via + * `standard()`). + * @param iMin Inclusive lower bound of the search range. + * @param iMax Exclusive upper bound of the search range. + * @return The dictionary index of @p strWord, or -1 if not found. + */ static int wsrch(std::string const& strWord, int iMin, int iMax); + + /** Decode 6 mnemonic words into an 8-byte binary block. + * + * Normalises each word, looks it up via `wsrch()`, packs the resulting + * 11-bit indices into a 9-byte buffer using `insert()`, then validates + * the 2-bit parity stored at bit offset 64. + * + * @param strData Output; receives the 8 decoded data bytes on success. + * Unchanged on any failure return. + * @param vsHuman Exactly 6 words to decode. Returns -1 immediately + * if the vector does not contain exactly 6 elements, or if any + * word is longer than 4 characters. + * @return 1 success. + * @return 0 a word was not found in the dictionary. + * @return -1 wrong word count or word exceeds 4 characters. + * @return -2 parity mismatch. + */ static int etob(std::string& strData, std::vector vsHuman); + /** The 2048-word mnemonic dictionary, sorted ascending. + * + * Indices 0–570 contain words of 1–3 characters; indices 571–2047 + * contain words of exactly 4 characters. This structural split is + * relied upon by `wsrch()` to restrict binary-search ranges. + */ static char const* dictionary[]; }; diff --git a/include/xrpl/crypto/csprng.h b/include/xrpl/crypto/csprng.h index e386d9d11e..8a0b2ff16a 100644 --- a/include/xrpl/crypto/csprng.h +++ b/include/xrpl/crypto/csprng.h @@ -4,14 +4,38 @@ namespace xrpl { -/** A cryptographically secure random number engine +/** @file + * Cryptographically secure pseudo-random number engine and singleton accessor. + * + * Every piece of key material in the XRP Ledger — wallet seeds, secret keys, + * nonces, session identifiers — is generated through `CsprngEngine`. The class + * is a thin, type-safe C++ wrapper around OpenSSL's `RAND_bytes` that provides + * thread safety and satisfies the C++ *UniformRandomNumberEngine* named + * requirement, allowing it to be used directly with standard-library facilities + * such as `std::uniform_int_distribution` and `beast::rngfill`. + */ - The engine is thread-safe (it uses a lock to serialize - access) and will, automatically, mix in some randomness - from std::random_device. - - Meets the requirements of UniformRandomNumberEngine -*/ +/** Cryptographically secure random number engine backed by OpenSSL. + * + * Wraps OpenSSL's `RAND_bytes` to provide randomness to the rest of the + * codebase without any caller needing to touch OpenSSL directly. Satisfies + * the C++ *UniformRandomNumberEngine* named requirement (`result_type`, + * `operator()()`, `min()`, `max()`), so it plugs directly into + * `std::uniform_int_distribution`, `beast::rngfill`, and similar utilities. + * + * Thread safety is version-conditioned at compile time: on OpenSSL ≥ 1.1.0 + * built with thread support, `RAND_bytes` is internally thread-safe and the + * per-call mutex acquisition is elided on the hot path. On older OpenSSL the + * mutex is always held. Entropy mixing (`mixEntropy`) always holds the mutex + * regardless of OpenSSL version because `RAND_add` modifies shared pool state. + * + * Copy and move operations are deleted. The engine holds a `std::mutex`, is + * backed by a global OpenSSL PRNG pool, and must be accessed as a singleton. + * Copying would produce a second object with no coherent relationship to that + * shared state. Use `cryptoPrng()` to obtain the singleton reference. + * + * @see cryptoPrng() + */ class CsprngEngine { private: @@ -28,29 +52,92 @@ public: CsprngEngine& operator=(CsprngEngine&&) = delete; + /** Construct and eagerly seed the engine. + * + * Calls `RAND_poll()` to harvest OS entropy (e.g., `/dev/urandom` on + * Linux, `CryptGenRandom` on Windows) before any bytes are generated. + * Although OpenSSL seeds itself lazily on first use, polling eagerly + * surfaces seeding failures at startup rather than during key generation. + * + * @throw std::runtime_error if `RAND_poll()` fails. + */ CsprngEngine(); + + /** Destroy the engine, releasing OpenSSL PRNG state on older runtimes. + * + * Calls `RAND_cleanup()` only for OpenSSL versions older than 1.1.0. + * Modern OpenSSL manages cleanup internally via `atexit`; calling + * `RAND_cleanup()` on those versions is unnecessary and was removed. + */ ~CsprngEngine(); - /** Mix entropy into the pool */ + /** Stir additional entropy into the OpenSSL random pool. + * + * Reads 128 values from `std::random_device` and passes them to + * `RAND_add` with an entropy estimate of zero. The caller-supplied + * buffer, if provided, is also added with a zero entropy estimate. + * The zero estimate is deliberate: on some platforms `std::random_device` + * may fall back to a software PRNG, so claiming zero entropy ensures + * OpenSSL's internal seeding threshold is never prematurely satisfied by + * potentially weak input. The data is still mixed into the pool. + * + * Called periodically from `Application.cpp` to stir in fresh OS entropy + * during the node's lifetime. May also be called with caller-supplied + * high-quality entropy from a hardware RNG or other trusted source. + * + * @param buffer Optional pointer to additional entropy material to mix in. + * Ignored if `nullptr` or if `count` is zero. + * @param count Number of bytes at `buffer` to mix in. + */ void mixEntropy(void* buffer = nullptr, std::size_t count = 0); - /** Generate a random integer */ + /** Generate a single random `result_type` value. + * + * Delegates to the buffer-fill overload with `sizeof(result_type)` bytes, + * sharing the same validation and error-handling path. + * + * @return A uniformly distributed random `std::uint64_t`. + * @throw std::runtime_error if the underlying `RAND_bytes` call fails + * (e.g., entropy pool exhausted). This is an unrecoverable condition; + * the exception is not caught by callers such as `randomSecretKey()`. + */ result_type operator()(); - /** Fill a buffer with the requested amount of random data */ + /** Fill a buffer with cryptographically secure random bytes. + * + * On OpenSSL ≥ 1.1.0 (built with thread support) the call to `RAND_bytes` + * is internally thread-safe and the mutex is elided at compile time. On + * older OpenSSL the mutex is held for the duration of the call. + * + * @param ptr Pointer to the buffer to fill; must not be `nullptr` when + * `count` is non-zero. + * @param count Number of random bytes to write into `ptr`. + * @throw std::runtime_error ("CSPRNG: Insufficient entropy") if + * `RAND_bytes` returns anything other than 1. Generating key material + * from an exhausted pool is a security failure, so the exception + * propagates and halts the operation. + */ void operator()(void* ptr, std::size_t count); - /* The smallest possible value that can be returned */ + /** Return the smallest value that `operator()()` can produce. + * + * Required by the *UniformRandomNumberEngine* named requirement. + * Always returns `std::numeric_limits::min()`. + */ static constexpr result_type min() { return std::numeric_limits::min(); } - /* The largest possible value that can be returned */ + /** Return the largest value that `operator()()` can produce. + * + * Required by the *UniformRandomNumberEngine* named requirement. + * Always returns `std::numeric_limits::max()`. + */ static constexpr result_type max() { @@ -58,14 +145,23 @@ public: } }; -/** The default cryptographically secure PRNG - - Use this when you need to generate random numbers or - data that will be used for encryption or passed into - cryptographic routines. - - This meets the requirements of UniformRandomNumberEngine -*/ +/** Return a reference to the process-wide cryptographically secure PRNG. + * + * Use this whenever random numbers or bytes are needed for cryptographic + * purposes: key generation, seed creation, nonce production, or any value + * passed into a cryptographic routine. The returned engine satisfies the + * C++ *UniformRandomNumberEngine* requirement and can be used directly with + * `std::uniform_int_distribution`, `beast::rngfill`, and similar utilities. + * + * The singleton is a Meyers-static local; C++11 guarantees thread-safe + * one-time construction, so the first call from any thread safely initialises + * the engine exactly once. Every caller shares the same OpenSSL PRNG pool. + * + * @return Reference to the process-wide `CsprngEngine` singleton. + * @note Never copy or store the returned reference by value — the deleted + * copy/move operations on `CsprngEngine` prevent this at compile time. + * @see CsprngEngine + */ CsprngEngine& cryptoPrng(); diff --git a/include/xrpl/crypto/secure_erase.h b/include/xrpl/crypto/secure_erase.h index 74284b03f7..39c500f040 100644 --- a/include/xrpl/crypto/secure_erase.h +++ b/include/xrpl/crypto/secure_erase.h @@ -1,23 +1,38 @@ +/** @file + * Declares `xrpl::secureErase`, the canonical primitive for wiping + * sensitive key material from memory in a way that survives compiler + * dead-store elimination. + */ + #pragma once #include namespace xrpl { -/** Attempts to clear the given blob of memory. - - The underlying implementation of this function takes pains to - attempt to outsmart the compiler from optimizing the clearing - away. Please note that, despite that, remnants of content may - remain floating around in memory as well as registers, caches - and more. - - For a more in-depth discussion of the subject please see the - below posts by Colin Percival: - - http://www.daemonology.net/blog/2014-09-04-how-to-zero-a-buffer.html - http://www.daemonology.net/blog/2014-09-06-zeroing-buffers-is-insufficient.html -*/ +/** Best-effort wipe of a memory region containing sensitive data. + * + * Overwrites `bytes` bytes starting at `dest` using `OPENSSL_cleanse`, + * which employs volatile writes or memory barriers to prevent the compiler + * from eliminating the store as a dead write. The function is defined in a + * separate translation unit (`secure_erase.cpp`) so the call is always + * opaque to the optimizer at the call site, reinforcing the effect. + * + * Use this instead of `memset` whenever clearing key material, seeds, or + * derived intermediates. The canonical pattern is to call it in destructors + * and immediately after copying raw key bytes into their final owner object. + * + * @param dest Pointer to the memory region to wipe. Must not be null and + * must point to at least `bytes` bytes of writable memory. + * @param bytes Number of bytes to overwrite. + * + * @note This is a best-effort mitigation, not a guarantee of complete + * erasure. Register contents, CPU caches, and other micro-architectural + * state are outside its reach. For a thorough discussion of the + * inherent limits see Colin Percival's analysis: + * http://www.daemonology.net/blog/2014-09-04-how-to-zero-a-buffer.html + * http://www.daemonology.net/blog/2014-09-06-zeroing-buffers-is-insufficient.html + */ void secureErase(void* dest, std::size_t bytes); diff --git a/include/xrpl/ledger/AcceptedLedgerTx.h b/include/xrpl/ledger/AcceptedLedgerTx.h index 0a1592f6e1..6dec21ce8e 100644 --- a/include/xrpl/ledger/AcceptedLedgerTx.h +++ b/include/xrpl/ledger/AcceptedLedgerTx.h @@ -10,66 +10,133 @@ namespace xrpl { -/** - A transaction that is in a closed ledger. - - Description - - An accepted ledger transaction contains additional information that the - server needs to tell clients about the transaction. For example, - - The transaction in JSON form - - Which accounts are affected - * This is used by InfoSub to report to clients - - Cached stuff -*/ +/** Immutable snapshot of a transaction accepted into a closed ledger. + * + * Constructed once from the closed ledger view, the serialized transaction, + * and its raw metadata; all downstream representations — JSON payload, binary + * metadata blob, affected-account set — are fully materialized at construction + * time and never recomputed. + * + * The pre-built `json_` payload is consumed directly by + * `NetworkOPsImp::pubValidatedTransaction()` and `pubAccountTransaction()` + * for WebSocket subscription delivery; `rawMeta_` (via `getEscMeta()`) feeds + * SQL `INSERT`/`REPLACE` statements in the relational transaction database. + * + * `CountedObject` inheritance exposes live-instance telemetry useful for + * detecting accumulation under load or slow subscriber drain holding ledger + * snapshots open longer than expected. + * + * @note Immutable after construction; safe to share across threads without + * additional locking. + * @note The ledger passed to the constructor must be closed (not open). + * Constructing from an open ledger aborts in debug builds. + * @see AcceptedLedger + */ class AcceptedLedgerTx : public CountedObject { public: + /** Construct and fully materialize a closed-ledger transaction snapshot. + * + * Parses metadata into a `TxMeta`, serializes raw metadata bytes, builds + * the complete JSON payload (transaction, meta, raw_meta, result, affected + * accounts), and — for non-self-funded `ttOFFER_CREATE` transactions — + * annotates the JSON with `owner_funds` queried from `accountFunds()` with + * freeze and auth checks bypassed. This avoids a later ledger round-trip + * when delivering to order-book subscribers. + * + * @param ledger The closed ledger that accepted this transaction. Must not + * be open; the constructor asserts `!ledger->open()` in debug builds. + * @param txn The serialized transaction object. + * @param met The raw metadata `STObject` produced during transaction apply. + */ AcceptedLedgerTx( std::shared_ptr const& ledger, std::shared_ptr const&, std::shared_ptr const&); + /** Returns the serialized transaction. */ [[nodiscard]] std::shared_ptr const& getTxn() const { return txn_; } + + /** Returns the parsed transaction metadata, including affected nodes and + * result code. + */ [[nodiscard]] TxMeta const& getMeta() const { return meta_; } + /** Returns the set of accounts affected by this transaction. + * + * Stored as a `flat_set` for cache-friendly iteration during subscription + * fan-out in `pubAccountTransaction()`. + */ [[nodiscard]] boost::container::flat_set const& getAffected() const { return affected_; } + /** Returns the transaction's unique identifier (SHA-512 half of the + * canonical serialization). + */ [[nodiscard]] TxID getTransactionID() const { return txn_->getTransactionID(); } + + /** Returns the transaction type (e.g., `ttOFFER_CREATE`, `ttPAYMENT`). */ [[nodiscard]] TxType getTxnType() const { return txn_->getTxnType(); } + + /** Returns the transaction result code as recorded in metadata. */ [[nodiscard]] TER getResult() const { return meta_.getResultTER(); } + + /** Returns the transaction's ordinal position within the closed ledger. + * + * This is `TxMeta::getIndex()` — the transaction's sequence number within + * the ledger's ordered transaction set, not the account sequence number. + */ [[nodiscard]] std::uint32_t getTxnSeq() const { return meta_.getIndex(); } + + /** Returns the raw metadata formatted as an escaped SQL blob literal. + * + * Formats `rawMeta_` via `sqlBlobLiteral()` for direct embedding in SQL + * `INSERT`/`REPLACE` statements (see `STTx::getMetaSQL()` in `Node.cpp`). + * + * @return SQL blob literal string suitable for verbatim inclusion in a + * SQL statement. + * @note Asserts that `rawMeta_` is non-empty. An empty blob indicates + * upstream ledger corruption; every accepted transaction must carry + * metadata. + */ [[nodiscard]] std::string getEscMeta() const; + /** Returns the pre-built JSON envelope for WebSocket subscription delivery. + * + * The object contains `transaction`, `meta`, `raw_meta` (hex), `result` + * (human-readable TER string), and `affected` (base58 account array). + * For non-self-funded `ttOFFER_CREATE` transactions, `transaction` also + * contains `owner_funds` — the account's spendable balance of the offered + * asset at acceptance time, computed with freeze and auth checks bypassed. + */ [[nodiscard]] json::Value const& getJson() const { diff --git a/include/xrpl/ledger/AmendmentTable.h b/include/xrpl/ledger/AmendmentTable.h index 8ed3cb81ff..83f66381c1 100644 --- a/include/xrpl/ledger/AmendmentTable.h +++ b/include/xrpl/ledger/AmendmentTable.h @@ -14,56 +14,172 @@ namespace xrpl { class ServiceRegistry; -/** The amendment table stores the list of enabled and potential amendments. - Individuals amendments are voted on by validators during the consensus - process. -*/ +/** Tracks enabled and pending amendments and coordinates validator voting. + * + * Each protocol change (amendment) must achieve an 80% supermajority of + * trusted validators for `majorityTime` before it activates. This class + * manages the full lifecycle: registration of supported amendments, vote + * aggregation across flag ledgers, pseudo-transaction injection at consensus + * time, and detection of "amendment blocked" conditions where the network has + * enabled a feature this node does not support. + * + * The interface is split into two layers. The pure virtual methods form the + * internal API that the concrete implementation satisfies, operating on + * pre-extracted amendment sets. Two concrete non-virtual adapter methods + * (`doValidatedLedger(shared_ptr)` and + * `doVoting(shared_ptr, ...)`) read amendment state from a + * `ReadView` and delegate to the pure-virtual overloads, keeping the + * implementation independent of the ledger view layer. + * + * @note Amendment voting is only meaningful at flag ledgers (multiples of + * 256). Use `needValidatedLedger` to gate the more expensive + * `doValidatedLedger` call. + * @see Feature.h for `VoteBehavior` and `majorityAmendments_t` + */ class AmendmentTable { public: + /** Metadata for a single registered amendment. + * + * Bundles the human-readable name, canonical 256-bit hash, and compiled-in + * vote preference for one amendment. Non-default-constructible: every + * instance must carry all three fields. + * + * @note Amendments with `VoteBehavior::Obsolete` are still registered so + * the node remains amendment-unblocked if the network enables them, but + * the node will never emit votes for them and their vote behavior cannot + * be overridden by config. + */ struct FeatureInfo { FeatureInfo() = delete; + + /** Construct a FeatureInfo with all required fields. + * + * @param n Human-readable amendment name (e.g., "OwnerPaysFee"). + * @param f Canonical 256-bit amendment hash used in ledger state and + * validations. + * @param v Compiled-in voting preference (`DefaultYes`, `DefaultNo`, + * or `Obsolete`). + */ FeatureInfo(std::string n, uint256 const& f, VoteBehavior v) : name(std::move(n)), feature(f), vote(v) { } + /** Human-readable name of the amendment. */ std::string const name; + + /** Canonical 256-bit amendment identifier used throughout the ledger. */ uint256 const feature; + + /** Compiled-in voting preference for this amendment. */ VoteBehavior const vote; }; virtual ~AmendmentTable() = default; + /** Look up an amendment's 256-bit hash by its human-readable name. + * + * @param name The amendment name to look up (case-sensitive). + * @return The amendment's `uint256` hash, or a zero value if no + * amendment with that name is registered. + */ [[nodiscard]] virtual uint256 find(std::string const& name) const = 0; + /** Suppress this node's vote for an amendment. + * + * Changes the amendment's vote from Up to Down regardless of the + * compiled-in `VoteBehavior`. May be called on amendments not in the + * supported list; an entry is created if one does not exist. The new + * state is persisted to the wallet database. + * + * @param amendment The 256-bit amendment hash to veto. + * @return `true` if the vote state changed (was Up, now Down); + * `false` if the amendment was already Down-voted or Obsolete. + */ virtual bool veto(uint256 const& amendment) = 0; + + /** Remove a previously applied veto for an amendment. + * + * Reverts the amendment's vote from Down back to Up. The change is + * persisted to the wallet database. Has no effect if the amendment + * was never vetoed, does not exist, or has `VoteBehavior::Obsolete` + * (Obsolete amendments cannot be unvetoed). + * + * @param amendment The 256-bit amendment hash to un-veto. + * @return `true` if the vote state changed (was Down, now Up); + * `false` if the amendment was not in the Down state. + */ virtual bool unVeto(uint256 const& amendment) = 0; + /** Mark an amendment as enabled in the local amendment table. + * + * Directly flips the amendment's enabled flag. Called by + * `doValidatedLedger` when ledger state confirms the amendment is active. + * If the amendment is not in the supported list, `hasUnsupportedEnabled()` + * will subsequently return `true`. + * + * @param amendment The 256-bit amendment hash to enable. + * @return `true` if the amendment was not already enabled; + * `false` if it was already in the enabled state. + */ virtual bool enable(uint256 const& amendment) = 0; + /** Return whether an amendment is currently active on the network. + * + * @param amendment The 256-bit amendment hash to query. + * @return `true` if the amendment has been enabled via `enable()` or + * through ledger validation; `false` otherwise. + */ [[nodiscard]] virtual bool isEnabled(uint256 const& amendment) const = 0; + + /** Return whether this node's software knows about and supports an amendment. + * + * @param amendment The 256-bit amendment hash to query. + * @return `true` if the amendment was included in the `supported` list + * passed to `makeAmendmentTable`; `false` for unknown amendments. + */ [[nodiscard]] virtual bool isSupported(uint256 const& amendment) const = 0; - /** - * @brief returns true if one or more amendments on the network - * have been enabled that this server does not support + /** Return whether any network-enabled amendment is unsupported by this node. * - * @return true if an unsupported feature is enabled on the network + * When this returns `true`, the node is "amendment blocked" — it is + * executing ledger rules it does not fully implement. The application + * layer should warn operators and eventually halt participation. + * + * @return `true` if at least one enabled amendment is not in this node's + * supported list. */ [[nodiscard]] virtual bool hasUnsupportedEnabled() const = 0; + /** Return the projected activation time of the earliest unsupported amendment. + * + * Scans amendments currently holding validator supermajority that are not + * supported by this node and returns the time at which the earliest such + * amendment is expected to activate (`majorityTime` after it first + * achieved supermajority). Updated by `doValidatedLedger`. + * + * @return The projected activation time of the first unsupported amendment + * that has achieved majority, or `std::nullopt` if no unsupported + * amendment is approaching activation. + */ [[nodiscard]] virtual std::optional firstUnsupportedExpected() const = 0; + /** Serialize all known amendments to JSON for RPC responses. + * + * @param isAdmin `true` to include sensitive or operator-only fields. + * @return A `json::Value` object containing the full amendment list with + * status, vote, and majority information for each entry. + */ [[nodiscard]] virtual json::Value getJson(bool isAdmin) const = 0; @@ -71,7 +187,17 @@ public: [[nodiscard]] virtual json::Value getJson(uint256 const& amendment, bool isAdmin) const = 0; - /** Called when a new fully-validated ledger is accepted. */ + /** Update amendment state from a newly validated ledger. + * + * Adapter that extracts `enabledAmendments` and `majorityAmendments` from + * `lastValidatedLedger` via `getEnabledAmendments()` and + * `getMajorityAmendments()`, then delegates to the pure-virtual + * `doValidatedLedger(LedgerIndex, set, majorityAmendments_t)` overload. + * The call is skipped entirely when `needValidatedLedger` returns `false`. + * + * @param lastValidatedLedger The most recently validated ledger. Amendment + * state is read from this view; the ledger sequence gates the update. + */ void doValidatedLedger(std::shared_ptr const& lastValidatedLedger) { @@ -84,24 +210,77 @@ public: } } - /** Called to determine whether the amendment logic needs to process - a new validated ledger. (If it could have changed things.) - */ + /** Return whether the amendment table needs to process a given ledger sequence. + * + * Amendment voting state only changes at flag ledgers (every 256 ledgers). + * This gate avoids the cost of extracting and processing amendment state + * for the vast majority of validated ledgers that cannot affect voting + * outcomes. + * + * @param seq The sequence number of the validated ledger being considered. + * @return `true` if `seq` crosses a new 256-ledger flag boundary relative + * to the last processed sequence; `false` if no change is possible. + */ [[nodiscard]] virtual bool needValidatedLedger(LedgerIndex seq) const = 0; + /** Update internal amendment state from pre-extracted ledger data. + * + * Enables all amendments in `enabled`, then scans `majority` for + * unsupported amendments approaching activation and updates the + * `firstUnsupportedExpected` projection accordingly. Errors are logged for + * each unsupported amendment that has reached supermajority. + * + * @param ledgerSeq Sequence number of the validated ledger. + * @param enabled Set of amendment hashes currently active in the ledger. + * @param majority Map of amendment hash → time of first observed + * supermajority for amendments that have crossed the voting threshold + * but are not yet enabled. + */ virtual void doValidatedLedger( LedgerIndex ledgerSeq, std::set const& enabled, majorityAmendments_t const& majority) = 0; - // Called when the set of trusted validators changes. + /** Notify the table that the set of trusted validators has changed. + * + * Updates the internal per-validator vote cache: existing records are + * preserved for validators that remain trusted; new validators are + * initialized with empty votes; validators no longer in the UNL have + * their records discarded. Vote history is NOT reset — this preserves + * the anti-flapping behavior that prevents an amendment from appearing to + * oscillate across the 80% threshold as validators come and go. + * + * @param allTrusted The complete current set of trusted validator public keys. + */ virtual void trustChanged(hash_set const& allTrusted) = 0; - // Called by the consensus code when we need to - // inject pseudo-transactions + /** Compute amendment actions for the current consensus round. + * + * Aggregates amendment votes from `valSet` against the current ledger + * state, applying the anti-flapping policy that retains the last known + * vote from each trusted validator for up to 24 hours. For each amendment + * whose vote state has changed relative to the ledger, produces an action + * entry: + * - `tfGotMajority` — validators have supermajority; ledger does not yet + * record it. + * - `tfLostMajority` — validators have lost supermajority; ledger still + * records it. + * - `0` — supermajority has been held for `majorityTime`; enable now. + * + * @param rules Protocol rules in effect for the ledger being built. + * @param closeTime Parent ledger's close time, used to evaluate whether + * `majorityTime` has elapsed since first supermajority. + * @param enabledAmendments Set of amendment hashes already active. + * @param majorityAmendments Map of amendment hash → time first achieving + * supermajority, for amendments not yet enabled. + * @param valSet Validations from the previous ledger; each carries the + * set of amendments the issuing validator supports. + * @return A map from amendment hash to action flag for each amendment + * requiring a pseudo-transaction in the initial consensus position. + */ virtual std::map doVoting( Rules const& rules, @@ -110,15 +289,27 @@ public: majorityAmendments_t const& majorityAmendments, std::vector> const& valSet) = 0; - // Called by the consensus code when we need to - // add feature entries to a validation + /** Return the amendment hashes this node wishes to vote for. + * + * Called when building a `STValidation` message. Returns all amendments + * that this node supports, has Up-voted, and that are not already active + * in the ledger. The result is sorted. + * + * @param enabled The set of amendment hashes currently enabled in the + * ledger; enabled amendments are excluded from the returned set. + * @return Sorted vector of amendment hashes this node wants to vote for. + */ [[nodiscard]] virtual std::vector doValidation(std::set const& enabled) const = 0; - // The set of amendments to enable in the genesis ledger - // This will return all known, non-vetoed amendments. - // If we ever have two amendments that should not both be - // enabled at the same time, we should ensure one is vetoed. + /** Return all non-vetoed amendments desired for a genesis ledger. + * + * Equivalent to `doValidation({})` — returns every supported, Up-voted + * amendment since none are enabled yet. If two amendments must not both be + * enabled simultaneously, one must be vetoed before calling this. + * + * @return All known, supported, non-vetoed amendment hashes. + */ [[nodiscard]] virtual std::vector getDesired() const = 0; @@ -128,6 +319,25 @@ public: // implementation. These APIs will merge when the view code // supports a full ledger API + /** Run the amendment voting pipeline and inject pseudo-transactions. + * + * Adapter for the consensus engine. Extracts amendment state from + * `lastClosedLedger`, delegates to the pure-virtual `doVoting` overload + * to determine required actions, then builds a signed-less `STTx` of type + * `ttAMENDMENT` for each action and inserts it into `initialPosition` + * as a `TnTransactionNm` node. These pseudo-transactions are not user + * transactions; they are injected directly into the consensus-agreed + * transaction set so validators can process them at flag-ledger close. + * + * @param lastClosedLedger The most recently closed ledger; supplies + * rules, parent close time, enabled amendments, and majority state. + * @param parentValidations Validations received for the parent ledger; + * each carries the voting validator's amendment preferences. + * @param initialPosition The SHAMap being built as the node's initial + * consensus position; amendment pseudo-transactions are added here. + * @param j Journal for debug logging of injected + * pseudo-transactions. + */ void doVoting( std::shared_ptr const& lastClosedLedger, @@ -169,6 +379,30 @@ public: } }; +/** Create the concrete AmendmentTable implementation. + * + * Registers all supported amendments, applies config-forced enables and + * vetoes, and loads any persisted vote overrides from the wallet database. + * Config entries in `enabled` and `vetoed` are ignored if the wallet database + * already contains a `FeatureVotes` table — the database is the authoritative + * source for persisted vote state. + * + * @param registry Service registry used to access the wallet database for + * persisting vote state. + * @param majorityTime Duration a supermajority must be continuously held + * before an amendment is enabled (typically two weeks on mainnet). + * @param supported All amendments compiled into this build, each with its + * `VoteBehavior`. Amendments absent from this list are treated as + * unsupported; enabling them sets `hasUnsupportedEnabled()`. + * @param enabled Config section (`[amendments]`) listing amendment IDs + * to force-enable; applied only when the wallet database has no + * `FeatureVotes` table. + * @param vetoed Config section (`[veto_amendments]`) listing amendment + * IDs to suppress votes for; applied only when the wallet database has no + * `FeatureVotes` table. + * @param journal Journal for logging during initialization. + * @return Owning pointer to the constructed `AmendmentTable`. + */ std::unique_ptr makeAmendmentTable( ServiceRegistry& registry, diff --git a/include/xrpl/ledger/ApplyView.h b/include/xrpl/ledger/ApplyView.h index f825311e1d..4d503a3f38 100644 --- a/include/xrpl/ledger/ApplyView.h +++ b/include/xrpl/ledger/ApplyView.h @@ -1,3 +1,14 @@ +/** @file + * Defines `ApplyView`, the writable ledger view used during transaction + * application, and the `ApplyFlags` bitmask that configures each apply pass. + * + * All state mutations produced by a transaction — trust-line updates, offer + * creation, account creation, fee destruction — flow through `ApplyView`. + * Changes are journaled and may be committed to the parent view or discarded + * atomically, enabling transactional rollback at every layer of the view + * hierarchy. + */ + #pragma once #include @@ -7,30 +18,54 @@ namespace xrpl { +/** Bitmask of flags that configure how a transaction apply pass behaves. + * + * Carried through every transaction-application call site. Multiple flags + * may be combined with `operator|`. All bitwise operators use `safeCast` + * to prevent silent conversion to the underlying integer type. + * + * @note Correctness and commutativity of `operator|` and `operator&` are + * verified by `static_assert` at compile time, guarding against future + * value collisions. + */ // Bitwise flag enum with existing operator overloads // NOLINTNEXTLINE(cppcoreguidelines-use-enum-class) enum ApplyFlags : std::uint32_t { + /** No flags set; default processing. */ TapNone = 0x00, - // This is a local transaction with the - // fail_hard flag set. + /** Transaction originated locally with `fail_hard` set. + * + * The engine must not retry; a hard failure that claims fees is + * produced instead of a soft retry. + */ TapFailHard = 0x10, - // This is not the transaction's last pass - // Transaction can be retried, soft failures allowed + /** This is not the transaction's final pass. + * + * Soft failures (insufficient balance, wrong sequence) are allowed + * because the transaction may succeed in a later pass. + */ TapRetry = 0x20, - // Transaction came from a privileged source + /** Transaction arrived from a trusted, privileged source. + * + * Certain per-transaction limits are relaxed (e.g., path count). + */ TapUnlimited = 0x400, - // Transaction is executing as part of a batch + /** Transaction is being processed as part of a batch transaction. */ TapBatch = 0x800, - // Transaction shouldn't be applied - // Signatures shouldn't be checked + /** Dry-run simulation: apply the transaction without committing state. + * + * Signature checks are skipped. A full `TxMeta` is still produced so + * callers can inspect the outcome. Used by the `simulate` RPC handler. + */ TapDryRun = 0x1000 }; +/** Combine two `ApplyFlags` values. */ constexpr ApplyFlags operator|(ApplyFlags const& lhs, ApplyFlags const& rhs) { @@ -42,6 +77,7 @@ operator|(ApplyFlags const& lhs, ApplyFlags const& rhs) static_assert((TapFailHard | TapRetry) == safeCast(0x30u), "ApplyFlags operator |"); static_assert((TapRetry | TapFailHard) == safeCast(0x30u), "ApplyFlags operator |"); +/** Mask `ApplyFlags` values, retaining only the bits present in both operands. */ constexpr ApplyFlags operator&(ApplyFlags const& lhs, ApplyFlags const& rhs) { @@ -53,6 +89,7 @@ operator&(ApplyFlags const& lhs, ApplyFlags const& rhs) static_assert((TapFailHard & TapRetry) == TapNone, "ApplyFlags operator &"); static_assert((TapRetry & TapFailHard) == TapNone, "ApplyFlags operator &"); +/** Invert all bits of an `ApplyFlags` value. */ constexpr ApplyFlags operator~(ApplyFlags const& flags) { @@ -61,6 +98,7 @@ operator~(ApplyFlags const& flags) static_assert(~TapRetry == safeCast(0xFFFFFFDFu), "ApplyFlags operator ~"); +/** Set-assign `ApplyFlags` bits from `rhs` into `lhs`. */ inline ApplyFlags operator|=(ApplyFlags& lhs, ApplyFlags const& rhs) { @@ -68,6 +106,7 @@ operator|=(ApplyFlags& lhs, ApplyFlags const& rhs) return lhs; } +/** Clear `ApplyFlags` bits in `lhs` that are absent from `rhs`. */ inline ApplyFlags operator&=(ApplyFlags& lhs, ApplyFlags const& rhs) { @@ -77,47 +116,40 @@ operator&=(ApplyFlags& lhs, ApplyFlags const& rhs) //------------------------------------------------------------------------------ -/** Writeable view to a ledger, for applying a transaction. - - This refinement of ReadView provides an interface where - the SLE can be "checked out" for modifications and put - back in an updated or removed state. Also added is an - interface to provide contextual information necessary - to calculate the results of transaction processing, - including the metadata if the view is later applied to - the parent (using an interface in the derived class). - The context info also includes values from the base - ledger such as sequence number and the network time. - - This allows implementations to journal changes made to - the state items in a ledger, with the option to apply - those changes to the base or discard the changes without - affecting the base. - - Typical usage is to call read() for non-mutating - operations. - - For mutating operations the sequence is as follows: - - // Add a new value - v.insert(sle); - - // Check out a value for modification - sle = v.peek(k); - - // Indicate that changes were made - v.update(sle) - - // Or, erase the value - v.erase(sle) - - The invariant is that insert, update, and erase may not - be called with any SLE which belongs to different view. -*/ +/** Writable view of a ledger used during transaction application. + * + * Extends `ReadView` with a checkout-modify-commit protocol: callers + * `peek()` an SLE to obtain a mutable handle, mutate it in place, then + * call `update()` (or `erase()`) to journal the change. `insert()` adds + * entries that were never checked out. All deltas are buffered; calling + * `apply()` on the concrete subclass flushes them to the parent view. + * Discarding the view without calling `apply()` abandons all changes. + * + * Also exposes directory management (`dirAppend`, `dirInsert`, `dirRemove`, + * `dirDelete`) and virtual payment hooks (`creditHookIOU`, `creditHookMPT`, + * `issuerSelfDebitHookMPT`, `adjustOwnerCountHook`) that `PaymentSandbox` + * overrides to prevent double-spend within a multi-hop payment path. + * + * @invariant `update()` and `erase()` must be called with an SLE obtained + * from `peek()` on **the same view instance**. Passing an SLE across + * view boundaries is undefined behavior, because each view journals its + * own deltas independently. + */ class ApplyView : public ReadView { private: - /** Add an entry to a directory using the specified insert strategy */ + /** Insert a key into the directory, routing to append-tail or + * sorted-insert logic based on `preserveOrder`. + * + * @param preserveOrder if `true`, append to tail (offer-book order); + * if `false`, insert in sorted position within each page. + * @param directory keylet of the directory root page. + * @param key the `uint256` key to insert. + * @param describe callback invoked on each newly allocated page SLE to + * brand it with type-specific fields (e.g., `sfOwner`). + * @return the 0-based page index where the key was stored, or + * `std::nullopt` if the page counter overflowed. + */ std::optional dirAdd( bool preserveOrder, @@ -128,92 +160,86 @@ private: public: ApplyView() = default; - /** Returns the tx apply flags. - - Flags can affect the outcome of transaction - processing. For example, transactions applied - to an open ledger generate "local" failures, - while transactions applied to the consensus - ledger produce hard failures (and claim a fee). - */ + /** Return the flags that govern this transaction apply pass. + * + * Flags shape engine behavior: `TapRetry` allows soft failures, + * `TapFailHard` demands a fee-claiming hard failure, `TapDryRun` + * suppresses state commits, and `TapUnlimited` relaxes per-tx limits. + * + * @return the `ApplyFlags` bitmask for this view. + */ [[nodiscard]] virtual ApplyFlags flags() const = 0; - /** Prepare to modify the SLE associated with key. - - Effects: - - Gives the caller ownership of a modifiable - SLE associated with the specified key. - - The returned SLE may be used in a subsequent - call to erase or update. - - The SLE must not be passed to any other ApplyView. - - @return `nullptr` if the key is not present - */ + /** Check out a ledger entry for in-place mutation. + * + * Returns an owning `shared_ptr` whose contents may be freely + * modified. The caller must pass the same pointer back to `update()` + * or `erase()` on **this** view instance when done; passing it to any + * other `ApplyView` is undefined behavior. + * + * @param k keylet identifying the entry. + * @return a mutable handle to the SLE, or `nullptr` if `k` is not + * present in this view. + */ virtual std::shared_ptr peek(Keylet const& k) = 0; - /** Remove a peeked SLE. - - Requirements: - - `sle` was obtained from prior call to peek() - on this instance of the RawView. - - Effects: - - The key is no longer associated with the SLE. - */ + /** Remove an entry previously checked out with `peek()`. + * + * Journals a delete delta so the entry is absent when this view's + * changes are later committed. + * + * @param sle a pointer obtained from `peek()` on this view instance. + * + * @note The key is taken from the SLE's own key field. + */ virtual void erase(std::shared_ptr const& sle) = 0; - /** Insert a new state SLE - - Requirements: - - `sle` was not obtained from any calls to - peek() on any instances of RawView. - - The SLE's key must not already exist. - - Effects: - - The key in the state map is associated - with the SLE. - - The RawView acquires ownership of the shared_ptr. - - @note The key is taken from the SLE - */ + /** Insert a brand-new ledger entry that has no prior existence in this view. + * + * The SLE must not have been obtained from `peek()`. Its key must not + * already exist in this view. The view takes ownership of the + * `shared_ptr`. + * + * @param sle the new entry to insert. + * + * @note The key is taken from the SLE's own key field. + */ virtual void insert(std::shared_ptr const& sle) = 0; - /** Indicate changes to a peeked SLE - - Requirements: - - The SLE's key must exist. - - `sle` was obtained from prior call to peek() - on this instance of the RawView. - - Effects: - - The SLE is updated - - @note The key is taken from the SLE - */ - /** @{ */ + /** Journal modifications to a checked-out ledger entry. + * + * Signals to the underlying delta table that the entry has changed and + * its new state must be written when this view's changes are committed. + * The entry's key must already exist. + * + * @param sle a pointer obtained from `peek()` on this view instance. + * + * @note The key is taken from the SLE's own key field. + */ virtual void update(std::shared_ptr const& sle) = 0; //-------------------------------------------------------------------------- - // Called when a credit is made to an account - // This is required to support PaymentSandbox + /** Notification hook invoked whenever an IOU credit is made to an account. + * + * The default implementation is a no-op; `PaymentSandbox` overrides it to + * record the credit in its `DeferredCredits` table so that subsequent + * `balanceHookIOU` calls subtract in-path credits from reported balances, + * preventing circular paths from manufacturing liquidity. + * + * @param from the debited account (sender side of the trust line). + * @param to the credited account (receiver side of the trust line). + * @param amount the IOU amount being credited; must hold an `Issue`. + * @param preCreditBalance the sender's trust-line balance before the credit. + * + * @note The `XRPL_ASSERT` in the default body verifies that `amount` holds + * an `Issue`; it fires in debug builds if the wrong asset type is passed. + */ virtual void creditHookIOU( AccountID const& from, @@ -224,6 +250,23 @@ public: XRPL_ASSERT(amount.holds(), "creditHookIOU: amount is for Issue"); } + /** Notification hook invoked whenever an MPT credit is made to an account. + * + * The default implementation is a no-op; `PaymentSandbox` overrides it to + * record the credit in its `DeferredCredits` table, enabling the same + * double-spend prevention as `creditHookIOU` but for MPT trust lines. + * + * @param from the debited account. + * @param to the credited account. + * @param amount the MPT amount being credited; must hold an `MPTIssue`. + * @param preCreditBalanceHolder the holder's MPT balance before the credit. + * @param preCreditBalanceIssuer the issuer's `OutstandingAmount` before the + * credit (signed to accommodate transient overflow). + * + * @note The `XRPL_ASSERT` in the default body verifies that `amount` holds + * an `MPTIssue`; it fires in debug builds if the wrong asset type is + * passed. + */ virtual void creditHookMPT( AccountID const& from, @@ -235,67 +278,66 @@ public: XRPL_ASSERT(amount.holds(), "creditHookMPT: amount is for MPTIssue"); } - /** Facilitate tracking of MPT sold by an issuer owning MPT sell offer. - * Unlike IOU, MPT doesn't have bi-directional relationship with an issuer, - * where a trustline limits an amount that can be issued to a holder. - * Consequently, the credit step (last MPTEndpointStep or - * BookStep buying MPT) might temporarily overflow OutstandingAmount. - * Limiting of a step's output amount in this case is delegated to - * the next step (in rev order). The next step always redeems when a holder - * account sells MPT (first MPTEndpointStep or BookStep selling MPT). - * In this case the holder account is only limited by the step's output - * and it's available funds since it's transferring the funds from one - * account to another account and doesn't change OutstandingAmount. - * This doesn't apply to an offer owned by an issuer. - * In this case the issuer sells or self debits and is increasing - * OutstandingAmount. Ability to issue is limited by the issuer - * originally available funds less already self sold MPT amounts (MPT sell - * offer). - * Consider an example: - * - GW creates MPT(USD) with 1,000USD MaximumAmount. - * - GW pays 950USD to A1. - * - A1 creates an offer 100XRP(buy)/100USD(sell). - * - GW creates an offer 100XRP(buy)/100USD(sell). - * - A2 pays 200USD to A3 with sendMax of 200XRP. - * Since the payment engine executes payments in reverse, - * OutstandingAmount overflows in MPTEndpointStep: 950 + 200 = 1,150USD. - * BookStep first consumes A1 offer. This reduces OutstandingAmount - * by 100USD: 1,150 - 100 = 1,050USD. GW offer can only be partially - * consumed because the initial available amount is 50USD = 1,000 - 950. - * BookStep limits it's output to 150USD. This in turn limits A3's send - * amount to 150XRP: A1 buys 100XRP and sells 100USD to A3. This doesn't - * change OutstandingAmount. GW buys 50XRP and sells 50USD to A3. This - * changes OutstandingAmount to 1,000USD. + /** Notification hook for MPT issuer self-debit via an owned sell offer. + * + * Unlike IOU trust lines, MPT has no bi-directional issuer↔holder + * relationship that caps issuance. When the payment engine processes a + * sell offer owned by the MPT issuer (in reverse order), it tentatively + * credits the holder first, which can transiently push `OutstandingAmount` + * beyond `MaximumAmount`. A subsequent step then redeems MPT from the + * issuer, restoring `OutstandingAmount`. The hook lets `PaymentSandbox` + * track the issuer's cumulative self-debit so that `balanceHookSelfIssueMPT` + * can cap available-to-issue at `origBalance − selfDebit` across the entire + * payment rather than trusting the transient ledger state. + * + * The default implementation is a no-op. + * + * @param issue the MPT issuance being self-debited. + * @param amount the quantity the issuer is selling (debiting to self). + * @param origBalance the issuer's `OutstandingAmount` at the start of the + * payment, before any path steps executed. */ virtual void issuerSelfDebitHookMPT(MPTIssue const& issue, std::uint64_t amount, std::int64_t origBalance) { } - // Called when the owner count changes - // This is required to support PaymentSandbox + /** Notification hook invoked when an account's owner count changes. + * + * The default implementation is a no-op; `PaymentSandbox` overrides it to + * record the high-water owner count for each account touched during the + * payment, so that reserve checks reflect the peak count rather than the + * instantaneous count at any single path step. + * + * @param account the account whose owner count is changing. + * @param cur the owner count before the change. + * @param next the owner count after the change. + */ virtual void adjustOwnerCountHook(AccountID const& account, std::uint32_t cur, std::uint32_t next) { } - /** Append an entry to a directory - - Entries in the directory will be stored in order of insertion, i.e. new - entries will always be added at the tail end of the last page. - - @param directory the base of the directory - @param key the entry to insert - @param describe callback to add required entries to a new page - - @return a \c std::optional which, if insertion was successful, - will contain the page number in which the item was stored. - - @note this function may create a page (including a root page), if no - page with space is available. This function will only fail if the - page counter exceeds the protocol-defined maximum number of - allowable pages. - */ + /** Append an entry to a directory, preserving insertion order. + * + * New entries are always placed at the tail of the last page, maintaining + * chronological ordering within an offer-book directory. This ordering + * is relied upon during offer matching: earlier offers at the same quality + * have priority. + * + * @param directory keylet of the directory root (page 0). + * @param key keylet of the entry to append; must be of type `ltOFFER`. + * @param describe callback invoked on each newly allocated page SLE to + * brand it with type-specific fields. + * @return the 0-based page index where the entry was stored, or + * `std::nullopt` if the page counter overflowed the protocol maximum. + * + * @note Only `ltOFFER` entries may be appended; passing any other keylet + * type triggers `UNREACHABLE` and returns `std::nullopt`. Use + * `dirInsert` for non-offer entries. + * @note A root page is created automatically if the directory does not yet + * exist. New pages are linked into the chain as needed. + */ /** @{ */ std::optional dirAppend( @@ -318,23 +360,24 @@ public: } /** @} */ - /** Insert an entry to a directory - - Entries in the directory will be stored in a semi-random order, but - each page will be maintained in sorted order. - - @param directory the base of the directory - @param key the entry to insert - @param describe callback to add required entries to a new page - - @return a \c std::optional which, if insertion was successful, - will contain the page number in which the item was stored. - - @note this function may create a page (including a root page), if no - page with space is available.this function will only fail if the - page counter exceeds the protocol-defined maximum number of - allowable pages. - */ + /** Insert an entry into a directory, maintaining per-page sorted order. + * + * Each individual page is kept in sorted key order, but entries may span + * multiple pages so the overall directory is only loosely ordered. + * Because legacy pages may not be sorted, each touched page is re-sorted + * before the new key is binary-inserted. Used for account-owned object + * directories (offers owned by an account, escrows, etc.). + * + * @param directory keylet of the directory root (page 0). + * @param key the `uint256` key to insert. + * @param describe callback invoked on each newly allocated page SLE to + * brand it with type-specific fields. + * @return the 0-based page index where the entry was stored, or + * `std::nullopt` if the page counter overflowed the protocol maximum. + * + * @note A root page is created automatically if the directory does not yet + * exist. New pages are allocated and linked as needed. + */ /** @{ */ std::optional dirInsert( @@ -345,6 +388,10 @@ public: return dirAdd(false, directory, key, describe); } + /** @copydoc dirInsert(Keylet const&, uint256 const&, std::function const&)> const&) + * + * Convenience overload that extracts the `uint256` key from `key.key`. + */ std::optional dirInsert( Keylet const& directory, @@ -355,25 +402,37 @@ public: } /** @} */ - /** Remove an entry from a directory - - @param directory the base of the directory - @param page the page number for this page - @param key the entry to remove - @param keepRoot if deleting the last entry, don't - delete the root page (i.e. the directory itself). - - @return \c true if the entry was found and deleted and - \c false otherwise. - - @note This function will remove zero or more pages from the directory; - the root page will not be deleted even if it is empty, unless - \p keepRoot is not set and the directory is empty. - */ + /** Remove a single entry from a directory and collapse any resulting + * empty non-root pages. + * + * After the key is removed, if the containing page becomes empty: + * - Non-root pages are unlinked and erased from the ledger. + * - The root page (page 0) is never erased unless `keepRoot` is `false` + * and the entire directory is now empty. + * - Legacy empty trailing pages left by older code are cleaned up + * opportunistically when the root page is touched. + * + * @param directory keylet of the directory root (page 0). + * @param page the 0-based page index that contains `key`; obtained from + * the page number stored in the owning ledger entry. + * @param key the `uint256` key to remove. + * @param keepRoot if `true`, preserve the root page even if it becomes + * empty after the removal (the directory anchor remains in the ledger). + * @return `true` if the entry was found and removed; `false` if the page + * or the key was not found. + * + * @note Throws `std::logic_error` if the directory linked-list pointers + * are found to be inconsistent (broken chain); this indicates ledger + * corruption and should never occur under normal operation. + */ /** @{ */ bool dirRemove(Keylet const& directory, std::uint64_t page, uint256 const& key, bool keepRoot); + /** @copydoc dirRemove(Keylet const&, std::uint64_t, uint256 const&, bool) + * + * Convenience overload that extracts the `uint256` key from `key.key`. + */ bool dirRemove(Keylet const& directory, std::uint64_t page, Keylet const& key, bool keepRoot) { @@ -381,31 +440,67 @@ public: } /** @} */ - /** Remove the specified directory, invoking the callback for every node. */ + /** Delete every page of a directory, invoking a callback for each key. + * + * Traverses the entire linked-list chain starting from page 0, erases + * each page SLE, and calls `callback` once per key stored in the + * directory. Callers are responsible for cleaning up the objects + * referenced by those keys before or after this call. + * + * @param directory keylet of the directory root (page 0). + * @param callback function called with each `uint256` key found in the + * directory before the page is erased. + * @return `true` if the root page was found and the directory was deleted; + * `false` if the root page does not exist. + */ bool dirDelete(Keylet const& directory, std::function const&); - /** Remove the specified directory, if it is empty. - - @param directory the identifier of the directory node to be deleted - @return \c true if the directory was found and was successfully deleted - \c false otherwise. - - @note The function should only be called with the root entry (i.e. with - the first page) of a directory. - */ + /** Delete the root page of a directory if and only if it is empty. + * + * Verifies that both `sfIndexes` is empty and the linked-list pointers + * indicate no other pages remain. Legacy empty trailing pages (a known + * edge case from older code) are cleaned up as a side effect before the + * emptiness check. + * + * @param directory keylet of the directory root page (`ltDIR_NODE`); + * must identify page 0 (the root). + * @return `true` if the directory was empty and was successfully erased; + * `false` if the directory was not found, contained entries, or had + * non-empty sub-pages. + * + * @note Throws `std::logic_error` if the directory linked-list pointers + * are inconsistent; this indicates ledger corruption. + */ bool emptyDirDelete(Keylet const& directory); }; -namespace directory { -/** Helper functions for managing low-level directory operations. - These are not part of the ApplyView interface. - - Don't use them unless you really, really know what you're doing. - Instead use dirAdd, dirInsert, etc. +/** Low-level primitives for building and modifying paged ledger directories. + * + * These helpers implement the individual steps of the directory linked-list + * protocol: root creation, tail-page discovery, key insertion, and page + * allocation. They are exposed so that specialised callers (tests, tooling) + * can exercise individual steps, but **transaction processors must always + * go through `ApplyView::dirAppend` / `dirInsert` / `dirRemove`** instead. + * + * @warning Do not call these directly unless you fully understand the + * directory invariants and page-linking protocol. */ +namespace directory { +/** Allocate and insert the root page (page 0) for a new directory. + * + * Creates a fresh `ltDIR_NODE` SLE at `directory`, sets `sfRootIndex`, + * calls `describe` to brand it, stores `key` as the first `sfIndexes` + * entry, and inserts it into the view. + * + * @param view the writable ledger view. + * @param directory keylet for the root page. + * @param key the first key to store in the new directory. + * @param describe callback to set type-specific fields on the root SLE. + * @return `0` — the root page index. + */ std::uint64_t createRoot( ApplyView& view, @@ -413,9 +508,37 @@ createRoot( uint256 const& key, std::function const&)> const& describe); +/** Locate the last used page in a directory by following `sfIndexPrevious` + * from the root. + * + * The root's `sfIndexPrevious` field always points to the tail page (O(1) + * append guarantee). If it is 0 the root itself is the tail. + * + * @param view the writable ledger view. + * @param directory keylet of the directory root. + * @param start the root SLE (already peeked by the caller). + * @return a tuple of `(pageIndex, pageSLE, sfIndexes)` for the tail page. + * @throws std::logic_error if the back-pointer chain is broken. + */ auto findPreviousPage(ApplyView& view, Keylet const& directory, SLE::ref start); +/** Insert a key into the `sfIndexes` vector of an existing page SLE and + * commit the change via `view.update()`. + * + * If `preserveOrder` is `true`, the key is appended at the end (offer-book + * order). If `false`, the page is sorted first (to handle legacy unsorted + * pages), then the key is binary-inserted. Double-insertion throws. + * + * @param view the writable ledger view. + * @param node the page SLE to modify (must have been obtained via `peek()`). + * @param page the 0-based page index of `node`. + * @param preserveOrder `true` to append; `false` to sort-then-insert. + * @param indexes the current `sfIndexes` vector (mutated in place). + * @param key the key to insert. + * @return the page index (`page`) where the key was stored. + * @throws std::logic_error if `key` is already present in `indexes`. + */ std::uint64_t insertKey( ApplyView& view, @@ -425,6 +548,26 @@ insertKey( STVector256& indexes, uint256 const& key); +/** Allocate a new trailing page, link it into the directory chain, and + * store the first key in it. + * + * The new page number is computed as `page + 1`; unsigned wraparound to 0 + * (verified by `static_assert`) signals overflow and causes `std::nullopt` + * to be returned. The `fixDirectoryLimit` amendment lifts the legacy + * per-directory page cap. + * + * @param view the writable ledger view. + * @param page the current last-page index (new page will be `page + 1`). + * @param node the current last-page SLE; its `sfIndexNext` is updated. + * @param nextPage reserved for future mid-chain insertion; must be `0`. + * @param next the root SLE; its `sfIndexPrevious` is updated to point to + * the new tail. + * @param key the first key to store on the new page. + * @param directory keylet of the directory root. + * @param describe callback to brand the new page SLE. + * @return the new page index, or `std::nullopt` on overflow or page-count + * limit violation. + */ std::optional insertPage( ApplyView& view, diff --git a/include/xrpl/ledger/ApplyViewImpl.h b/include/xrpl/ledger/ApplyViewImpl.h index 7f790f2be5..b3deaafca6 100644 --- a/include/xrpl/ledger/ApplyViewImpl.h +++ b/include/xrpl/ledger/ApplyViewImpl.h @@ -7,12 +7,25 @@ namespace xrpl { -/** Editable, discardable view that can build metadata for one tx. - - Iteration of the tx map is delegated to the base. - - @note Presented as ApplyView to clients. -*/ +/** Per-transaction scratch-pad view that buffers ledger mutations and + * constructs `TxMeta` on commit. + * + * `ApplyViewImpl` is the concrete view handed to every `Transactor` + * during the apply phase. It sits at the top of the view hierarchy: + * `ReadView` → `ApplyView` → `detail::ApplyViewBase` → `ApplyViewImpl`. + * All ledger mutations are buffered in the inherited `items_` + * (`ApplyStateTable`) and are not visible to the parent `OpenView` + * until `apply()` is called. If the transaction fails, the view is + * discarded and `base_` is left unchanged. + * + * The object is move-constructible but neither copyable nor + * move-assignable, ensuring that at most one instance can commit a + * given transaction's buffered state. + * + * @note `base_` is held as a raw `const*` (not a shared pointer) for + * performance. The caller must ensure the underlying view outlives + * this object. + */ class ApplyViewImpl final : public detail::ApplyViewBase { public: @@ -24,14 +37,47 @@ public: operator=(ApplyViewImpl const&) = delete; ApplyViewImpl(ApplyViewImpl&&) = default; + + /** Construct a transaction apply view over an existing read view. + * + * @param base The underlying ledger state to read from. Must remain + * valid for the lifetime of this object. + * @param flags Apply-phase control flags (e.g., `tapRETRY`, + * `tapDRY_RUN`, `tapBATCH`) that influence commit behavior and + * the metadata produced by `apply()`. + */ ApplyViewImpl(ReadView const* base, ApplyFlags flags); - /** Apply the transaction. - - After a call to `apply`, the only valid - operation on this object is to call the - destructor. - */ + /** Flush buffered mutations to `to` and produce transaction metadata. + * + * Delegates to `ApplyStateTable::apply()`, which drains every + * pending insert, modify, and erase into `to` and builds the + * `TxMeta` record — including `sfCreatedNode`, `sfModifiedNode`, + * and `sfDeletedNode` entries with `sfPreviousFields`/`sfFinalFields` + * — for the closed ledger. If `isDryRun` is `true`, metadata is + * computed and returned but state changes are suppressed, supporting + * fee-simulation paths without side effects. + * + * When `parentBatchId` is set (i.e., `tapBATCH` is active), the + * generated metadata records the parent batch transaction ID so + * individual results can be traced back to their enclosing batch. + * + * @param to The target open view that accumulates all + * committed transaction changes for the current ledger round. + * @param tx The transaction being applied. + * @param ter The final result code; recorded in metadata. + * @param parentBatchId The ID of the enclosing `ttBATCH` transaction, + * or `std::nullopt` for standalone transactions. + * @param isDryRun If `true`, produce metadata without mutating `to`. + * @param j Journal for diagnostic logging. + * @return The `TxMeta` for closed-ledger commits and dry-run + * evaluation; `std::nullopt` when `to` is still open and + * `isDryRun` is `false`. + * + * @note After this call returns, the only valid operation on this + * object is destruction. The internal `ApplyStateTable` is + * drained and must not be accessed again. + */ std::optional apply( OpenView& to, @@ -41,25 +87,50 @@ public: bool isDryRun, beast::Journal j); - /** Set the amount of currency delivered. - - This value is used when generating metadata - for payments, to set the DeliveredAmount field. - If the amount is not specified, the field is - excluded from the resulting metadata. - */ + /** Record the amount delivered by a payment transaction. + * + * Stores `amount` so that `ApplyStateTable::apply()` can write the + * `sfDeliveredAmount` field into the resulting `TxMeta`. The + * delivered amount can differ from the send amount in cross-currency + * or partial-payment scenarios. If never called, `sfDeliveredAmount` + * is omitted from the metadata. + * + * Must be called before `apply()` to take effect. + * + * @param amount The currency amount actually received by the destination. + */ void deliver(STAmount const& amount) { deliver_ = amount; } - /** Get the number of modified entries + /** Return the number of pending write-intent entries. + * + * Counts only entries with an `Erase`, `Insert`, or `Modify` action; + * cache-only reads are excluded. Used by `ApplyContext::size()` to + * support batch-processing decisions before committing. + * + * @return Count of SLE mutations buffered since construction or the + * last `discard()`. */ std::size_t size(); - /** Visit modified entries + /** Iterate every pending write-intent entry, invoking a callback per entry. + * + * Delegates to `ApplyStateTable::visit()`. `Cache`-only reads are + * skipped. Used by invariant checkers and batch-processing logic to + * inspect accumulated changes before deciding whether to commit them. + * + * @param target The open view used to fetch pre-change SLE snapshots + * for `Erase` and `Modify` entries. + * @param func Callback invoked once per pending entry with: + * - `key` — ledger index of the entry. + * - `isDelete` — `true` if the entry is being erased. + * - `before` — the SLE state before this transaction (`nullptr` + * for insertions). + * - `after` — the pending SLE state (`nullptr` for deletions). */ void visit( diff --git a/include/xrpl/ledger/BookDirs.h b/include/xrpl/ledger/BookDirs.h index afde6ee7b3..f1bc170bb9 100644 --- a/include/xrpl/ledger/BookDirs.h +++ b/include/xrpl/ledger/BookDirs.h @@ -4,6 +4,25 @@ namespace xrpl { +/** A range-based view over all offers in a single order-book direction. + * + * The XRPL DEX stores offers in a two-level ledger directory structure: a + * *book* groups offers for one currency pair in one direction, and within + * the book each quality level (encoded exchange rate) has its own directory + * page. `BookDirs` presents this multi-level structure as a flat sequence of + * `SLE` objects, letting callers iterate every offer with a standard + * range-for loop without reasoning about quality boundaries or directory + * pagination. + * + * Construction eagerly locates the first quality directory via + * `ReadView::succ` and loads the first page with `cdirFirst`; subsequent + * advancement is handled lazily by `const_iterator::operator++`. + * + * @note The `ReadView` passed at construction must outlive both the + * `BookDirs` object and any iterators derived from it. Iterators hold + * raw pointers to the view. + * @see Dir for single-directory iteration (e.g., NFTokenOffer pages). + */ class BookDirs { private: @@ -19,15 +38,49 @@ public: class const_iterator; // NOLINT(readability-identifier-naming) using value_type = std::shared_ptr; + /** Construct a `BookDirs` range over all offers in `book` as seen by `view`. + * + * Finds the first quality directory in the book's key-space via + * `view.succ` and positions the internal state at the first offer. + * If the book is empty, `begin() == end()` immediately. + * + * @param view The ledger view to read from; must remain valid for the + * lifetime of this object and all derived iterators. + * @param book The currency pair and direction defining the order book. + */ BookDirs(ReadView const&, Book const&); + /** Return an iterator positioned at the first offer in the book. + * + * If the book is empty the returned iterator compares equal to `end()`. + */ [[nodiscard]] const_iterator begin() const; + /** Return the past-the-end sentinel iterator for this book. */ [[nodiscard]] const_iterator end() const; }; +/** Forward iterator over offers in an order book, crossing quality boundaries. + * + * Advances through all offers in a book by walking pages within each quality + * directory via `cdirNext`, then locating the next quality directory via + * `ReadView::succ` when a quality is exhausted. Dereference re-reads the + * current offer SLE from the view and caches it until `operator++` clears + * the cache. + * + * **End-sentinel encoding:** the end iterator and an exhausted begin iterator + * share identical state — `entry_ == 0`, `cur_key_ == key_`, and + * `index_ == beast::zero`. `operator++` explicitly resets to this state when + * no further quality directory exists, which is how loop termination is + * detected. + * + * @note Default-constructed iterators have a null `view_` and compare + * unequal to everything, including each other. They are valid only as + * placeholders; dereferencing them is undefined behaviour. + * @note Only `BookDirs` may construct iterators in valid, non-default states. + */ class BookDirs::const_iterator // NOLINT(readability-identifier-naming) { public: @@ -37,35 +90,91 @@ public: using difference_type = std::ptrdiff_t; using iterator_category = std::forward_iterator_tag; + /** Construct a default (placeholder) iterator with a null view. + * + * Required by the `ForwardIterator` concept. The resulting iterator + * compares unequal to all other iterators and must not be dereferenced + * or incremented. + */ const_iterator() = default; + /** Return true if both iterators refer to the same offer position. + * + * Equality is determined by comparing `entry_`, `cur_key_`, and + * `index_`. If either iterator has a null view, returns false. + * + * @note Comparing iterators from different `BookDirs` instances + * (different views or roots) triggers an assertion in debug builds. + */ bool operator==(const_iterator const& other) const; + /** Return true if the iterators do not refer to the same offer position. */ bool operator!=(const_iterator const& other) const { return !(*this == other); } + /** Return a reference to the current offer SLE. + * + * Reads the offer SLE from the view on first access and caches the + * result; subsequent dereferences of the same position return the cached + * value. The cache is cleared by `operator++`. + * + * @note Asserts that `index_` is non-zero; dereferencing the end + * iterator or a default-constructed iterator is undefined behaviour. + */ reference operator*() const; + /** Return a pointer to the current offer SLE. + * + * Equivalent to `&**this`. Safe to use with `->` because `operator*` + * stores the result in a `mutable` cache member whose lifetime matches + * the iterator. + */ pointer operator->() const { return &**this; } + /** Advance to the next offer in the book and return this iterator. + * + * First attempts to advance within the current quality directory via + * `cdirNext`. If that quality is exhausted, uses `ReadView::succ` to + * find the next quality directory and positions at its first offer via + * `cdirFirst`. If no further quality directory exists, resets to the + * end-sentinel state. Clears the dereference cache. + * + * @note Asserts that the iterator is not already at the end position + * (i.e. `index_` must be non-zero) before advancing. + */ const_iterator& operator++(); + /** Post-increment: advance and return a copy of the pre-increment state. */ const_iterator operator++(int); private: friend class BookDirs; + /** Construct a valid iterator anchored to `view`, `root`, and `dirKey`. + * + * Only `BookDirs` calls this constructor. `dirKey` becomes both `key_` + * (the end-sentinel anchor) and the initial `cur_key_`. Additional + * fields (`next_quality_`, `sle_`, `entry_`, `index_`) are populated by + * `BookDirs::begin()` for the begin iterator; the end iterator leaves + * them at their zero-initialised defaults. + * + * @param view The ledger view; must outlive this iterator. + * @param root The root key of the book's quality key-space; must be + * non-zero. + * @param dirKey The key of the first quality directory, or `beast::zero` + * if the book is empty. + */ const_iterator(ReadView const& view, uint256 const& root, uint256 const& dirKey) : view_(&view), root_(root), key_(dirKey), cur_key_(dirKey) { diff --git a/include/xrpl/ledger/BookListeners.h b/include/xrpl/ledger/BookListeners.h index 3b96aca680..0ef82b4f47 100644 --- a/include/xrpl/ledger/BookListeners.h +++ b/include/xrpl/ledger/BookListeners.h @@ -8,35 +8,71 @@ namespace xrpl { -/** Listen to public/subscribe messages from a book. */ +/** Per-book fan-out layer for WebSocket order-book subscriptions. + * + * One instance exists for each `Book` (currency pair) that has at least one + * active subscriber. `OrderBookDB` owns and looks up instances via + * `getBookListeners()` / `makeBookListeners()`; callers hold references + * through the `pointer` alias. + * + * Subscribers are stored as `InfoSub::wptr` (weak pointers) so that + * `BookListeners` does not extend the lifetime of the connection object. + * Dead entries are pruned lazily inside `publish()` when the weak pointer + * can no longer be locked. + * + * All three public methods take `lock_` for their full duration, including + * across the `p->send()` calls in `publish()`. This favours correctness over + * throughput on high-subscriber-count books. + */ class BookListeners { public: + /** Shared-ownership handle used by `OrderBookDB` and callers. */ using pointer = std::shared_ptr; BookListeners() = default; - /** Add a new subscription for this book + /** Register a subscriber for this book. + * + * Stores a weak pointer to @p sub, keyed by its sequence number, so that + * subsequent `publish()` calls deliver notifications to it. + * + * @param sub The subscriber to add; must not be null. */ void addSubscriber(InfoSub::ref sub); - /** Stop publishing to a subscriber + /** Unregister a subscriber by sequence number. + * + * Removes the entry whose key equals @p sub. If no such entry exists + * (e.g. the subscriber was already pruned by a `publish()` call after + * disconnect), this is a no-op. + * + * @param sub Sequence number returned by `InfoSub::getSeq()` for the + * subscriber to remove. */ void removeSubscriber(std::uint64_t sub); - /** Publish a transaction to subscribers - - Publish a transaction to clients subscribed to changes on this book. - Uses havePublished to prevent sending duplicate transactions to clients - that have subscribed to multiple books. - - @param jvObj JSON transaction data to publish - @param havePublished InfoSub sequence numbers that have already - published this transaction. - - */ + /** Deliver a transaction notification to all live subscribers. + * + * Iterates over the internal listener map and, for each live subscriber, + * attempts to insert its sequence number into @p havePublished. If the + * insertion succeeds (i.e. this subscriber has not yet received this + * transaction from another book), the version-appropriate JSON is + * dispatched via `InfoSub::send()`. + * + * Dead weak pointers (subscribers that have disconnected) are erased + * in-place during the scan, providing lazy GC without a separate sweep. + * + * @param jvObj Version-indexed JSON built once upstream; each subscriber + * receives the slice matching its negotiated API version. + * @param havePublished Per-transaction set of subscriber sequence numbers + * that have already been notified. Shared across every + * `BookListeners::publish()` call for the same transaction so that a + * client subscribed to multiple affected books receives the message + * only once. Passed by reference and mutated in-place. + */ void publish(MultiApiJson const& jvObj, hash_set& havePublished); diff --git a/include/xrpl/ledger/CachedSLEs.h b/include/xrpl/ledger/CachedSLEs.h index c05aab8856..95d44f3fc3 100644 --- a/include/xrpl/ledger/CachedSLEs.h +++ b/include/xrpl/ledger/CachedSLEs.h @@ -1,3 +1,12 @@ +/** @file + * Process-wide cache of deserialized ledger state entries (SLEs). + * + * Declares `CachedSLEs`, a named alias for the `TaggedCache` instantiation + * that backs the two-level SLE read cache used by `CachedView`. Any future + * change to the underlying container's key hasher, pointer policy, or mutex + * type can be made here without touching consumers. + */ + #pragma once #include @@ -5,5 +14,32 @@ #include namespace xrpl { + +/** Process-wide, thread-safe cache of immutable ledger state entries (SLEs). + * + * Maps the cryptographic digest of a serialized SLE (`uint256`) to the + * deserialized `SLE const` object, allowing multiple read paths to share a + * single in-memory representation without re-deserializing from disk. + * + * The `SLE const` mapped type enforces at compile time that stored objects + * are never mutated through the cache, satisfying `TaggedCache`'s requirement + * that callers must not modify stored objects unless they hold a lock over all + * cache operations. This makes cached entries safe to share across threads + * without additional per-object locking. + * + * The key is the on-disk hash (digest) of the serialized entry — not an + * account ID or keylet — which integrates directly with `DigestAwareReadView`. + * `CachedView` delegates `read()` calls to `CachedSLEs::fetch(digest, ...)`, + * falling through to the underlying store only on a miss. + * + * The application-wide instance is constructed with a target size of `0` + * (no fixed count limit) and a one-minute expiration window. + * `TaggedCache::sweep()` is called periodically to demote strong references + * to weak references and eventually reclaim memory. + * + * @see CachedView + * @see TaggedCache + */ using CachedSLEs = TaggedCache; + } // namespace xrpl diff --git a/include/xrpl/ledger/CachedView.h b/include/xrpl/ledger/CachedView.h index 34a75e4c07..30c0436a60 100644 --- a/include/xrpl/ledger/CachedView.h +++ b/include/xrpl/ledger/CachedView.h @@ -1,3 +1,16 @@ +/** @file + * Transparent two-level caching layer over a `DigestAwareReadView`. + * + * Declares `detail::CachedViewImpl` (non-template caching logic) and the + * public template `CachedView`, which adds `shared_ptr` ownership of + * the wrapped view. The canonical instantiation `CachedLedger` (defined in + * `Ledger.h`) wraps the immutable closed ledger that serves as the base for + * transaction application. + * + * @see CachedSLEs + * @see CachedLedger + */ + #pragma once #include @@ -11,12 +24,43 @@ namespace xrpl { namespace detail { +/** Non-template base class that implements SLE caching over a `DigestAwareReadView`. + * + * All caching logic is compiled once here, avoiding template-instantiation bloat + * in `CachedView`. The class maintains two complementary caches: + * + * - **`map_`** — a per-instance `unordered_map` from ledger key (`uint256`) to + * SLE digest. Once a key has been resolved to its content hash, subsequent + * reads skip the SHAMap traversal. Uses `HardenedHash<>` to resist + * hash-flood attacks from adversarially crafted ledger keys. + * - **`cache_`** — a reference to an externally owned, process-wide `CachedSLEs` + * (`TaggedCache`) keyed by digest. Multiple views over + * different ledgers share this cache; if two ledgers carry an unchanged SLE, + * only one deserialized copy lives in memory. + * + * `mutex_` guards `map_` only; it is deliberately *not* held across + * `base_.digest()` or `base_.read()` calls so that concurrent readers are not + * serialized through SHAMap traversal or deserialization. Two threads may both + * call `base_.digest()` for the same key on a cold miss — this is safe because + * `base_` is an immutable ledger snapshot. + * + * Copy and assignment are deleted; a cached view always represents a unique, + * coherent window onto a specific ledger snapshot. + * + * @note All `ReadView` and `DigestAwareReadView` pass-through methods delegate + * directly to `base_`; only `exists()` and `read()` go through the cache. + */ class CachedViewImpl : public DigestAwareReadView { private: DigestAwareReadView const& base_; CachedSLEs& cache_; std::mutex mutable mutex_; + /** Per-instance map from ledger key to SLE digest. + * + * Uses `HardenedHash<>` to prevent adversarial hash-bucket flooding from + * network-visible ledger keys (account IDs, object types). + */ std::unordered_map> mutable map_; public: @@ -25,6 +69,13 @@ public: CachedViewImpl& operator=(CachedViewImpl const&) = delete; + /** Construct over an existing `DigestAwareReadView` and a shared SLE cache. + * + * @param base The underlying immutable view to cache reads against. + * The caller is responsible for ensuring `base` outlives this object; + * `CachedView` satisfies this by holding the owning `shared_ptr`. + * @param cache The process-wide SLE cache shared across all views. + */ CachedViewImpl(DigestAwareReadView const* base, CachedSLEs& cache) : base_(*base), cache_(cache) { } @@ -33,9 +84,30 @@ public: // ReadView // + /** Returns `true` if an SLE exists for the given keylet. + * + * Delegates to `read(k) != nullptr`; benefits from caching on repeated + * calls for the same key. + */ bool exists(Keylet const& k) const override; + /** Return the SLE associated with the keylet, going through both cache levels. + * + * The lookup sequence is: + * 1. Check `map_` for a known digest (under `mutex_`). + * 2. If absent, call `base_.digest(k.key)` outside the lock. + * 3. Pass the digest to `cache_.fetch()`, which deserializes from `base_` + * only on a shared-cache miss. + * 4. Populate `map_` on a cold miss (re-acquires `mutex_`). + * 5. Validate the SLE type with `k.check(*sle)`. + * + * Hit/miss statistics are tracked via `CountedObjects` counters + * `CachedView::hit`, `CachedView::hitExpired`, and `CachedView::miss`. + * + * @return The matching `SLE const`, or `nullptr` if the key is absent or + * the stored type does not match the keylet's expected type. + */ std::shared_ptr read(Keylet const& k) const override; @@ -124,10 +196,25 @@ public: } // namespace detail -/** Wraps a DigestAwareReadView to provide caching. - - @tparam Base A subclass of DigestAwareReadView -*/ +/** Transparent caching layer over a `DigestAwareReadView`. + * + * Wraps a `shared_ptr` to ensure the underlying view remains alive + * for the lifetime of this object, then delegates all caching logic to + * `detail::CachedViewImpl`. The `static_assert` enforces that `Base` satisfies + * the `DigestAwareReadView` contract required for two-level caching. + * + * The production instantiation is `CachedLedger = CachedView`, used + * by `OpenLedger::create()` to wrap the closed ledger that forms the base for + * each round of transaction application. + * + * Copy and assignment are deleted; each `CachedView` instance is the sole + * owner of its per-instance key→digest `map_`. + * + * @tparam Base A type derived from `DigestAwareReadView`. + * + * @see detail::CachedViewImpl + * @see CachedSLEs + */ template class CachedView : public detail::CachedViewImpl { @@ -144,15 +231,27 @@ public: CachedView& operator=(CachedView const&) = delete; + /** Construct a caching view over a shared immutable ledger snapshot. + * + * @param base Shared ownership of the underlying view; must not be null. + * @param cache Process-wide SLE cache shared across all `CachedView` + * instances. Must outlive this object. + */ CachedView(std::shared_ptr const& base, CachedSLEs& cache) : CachedViewImpl(base.get(), cache), sp_(base) { } - /** Returns the base type. - - @note This breaks encapsulation and bypasses the cache. - */ + /** Return the underlying view, bypassing both cache levels. + * + * @note This breaks encapsulation: callers interact with the + * `DigestAwareReadView` directly, skipping both the per-instance + * key→digest `map_` and the shared `CachedSLEs`. Use only when the + * full `Base` type (e.g. `Ledger`) is needed and cannot be expressed + * through the `ReadView` interface alone. + * + * @return A const shared pointer to the wrapped `Base` instance. + */ std::shared_ptr const& base() const { diff --git a/include/xrpl/ledger/CanonicalTXSet.h b/include/xrpl/ledger/CanonicalTXSet.h index 8653816eee..0ca3b2bb8c 100644 --- a/include/xrpl/ledger/CanonicalTXSet.h +++ b/include/xrpl/ledger/CanonicalTXSet.h @@ -7,17 +7,51 @@ namespace xrpl { -/** Holds transactions which were deferred to the next pass of consensus. - - "Canonical" refers to the order in which transactions are applied. - - - Puts transactions from the same account in SeqProxy order - -*/ +/** Ordered transaction queue for deterministic consensus application. + * + * Holds transactions deferred from a previous ledger-building pass and + * re-applies them in the next pass. The "canonical" in the name is the + * ordering guarantee: given the same input transaction set and the same + * salt, every validator iterates and applies transactions in identical + * sequence, which is required for Byzantine fault-tolerant ledger + * agreement. + * + * Ordering is three-level (implemented in `Key::operator<`): + * 1. Salted account ID — groups all transactions from the same account. + * 2. `SeqProxy` — within an account, sequence-based transactions sort + * before ticket-based ones, preserving the dependency that a ticket + * creator must apply before ticket consumers. + * 3. Transaction ID — tiebreaker within the same account and sequence. + * + * @note Account keys are XORed with a `LedgerHash` salt at construction + * (and via `reset()`) so that no actor can mine account addresses to + * achieve a persistent early-sort advantage across ledger rounds. + * + * @note Inherits from `CountedObject` for diagnostic + * memory-pressure accounting; the instance count is queryable via + * `CountedObjects::getInstance().getCounts()` and has no effect on + * behavior. + * + * Usage in `BuildLedger.cpp`: `applyTransactions()` iterates this set in + * map order across multiple passes, erasing each transaction on success or + * definitive failure and leaving retryable ones in place for the next pass. + */ // VFALCO TODO rename to SortedTxSet class CanonicalTXSet : public CountedObject { private: + /** Sort key for the internal transaction map. + * + * Holds a salted account identifier, a `SeqProxy`, and the transaction + * hash. The three-level `operator<` groups transactions by account, then + * orders within an account by `SeqProxy` (sequences before tickets), then + * breaks ties by transaction ID. + * + * `operator==` compares only `txId_` — identity is the transaction hash + * alone, independent of account or sequence context. This asymmetry is + * intentional: iterator-based `erase` must not conflate distinct + * transactions that happen to share account/sequence metadata. + */ class Key { public: @@ -47,6 +81,14 @@ private: return !(lhs < rhs); } + /** Tests equality by transaction ID only. + * + * Deliberately asymmetric with `operator<`: two keys with different + * account/sequence values but the same `txId_` compare equal. This + * keeps iterator-based removal (`erase`) safe — the map's ordering + * key is account+seq+id, but uniqueness is solely the transaction + * hash. + */ friend bool operator==(Key const& lhs, Key const& rhs) { @@ -59,12 +101,14 @@ private: return !(lhs == rhs); } + /** Returns the salted account identifier used as the primary sort key. */ [[nodiscard]] uint256 const& getAccount() const { return account_; } + /** Returns the transaction hash. */ [[nodiscard]] uint256 const& getTXID() const { @@ -80,7 +124,14 @@ private: friend bool operator<(Key const& lhs, Key const& rhs); - // Calculate the salted key for the given account + /** Computes the salted sort key for an account. + * + * Copies the 20-byte `AccountID` into a zeroed `uint256`, then XORs the + * result with `salt_`. The XOR prevents an attacker from mining account + * addresses with low byte values to gain a persistent ordering advantage: + * because `salt_` changes each ledger round, the effective sort position + * of any account is randomized per round. + */ uint256 accountKey(AccountID const& account); @@ -88,23 +139,59 @@ public: using const_iterator = std::map>::const_iterator; public: + /** Constructs the set with the given ledger hash as the account-key salt. + * + * @param saltHash Hash of the current ledger (or consensus map); used to + * randomize per-round account sort positions. Pass `uint256{}` when a + * stable, unsalted ordering is acceptable (e.g., `LocalTxs`). + */ explicit CanonicalTXSet(LedgerHash const& saltHash) : salt_(saltHash) { } + /** Inserts a transaction into the set. + * + * Constructs a `Key` from the transaction's salted account ID, `SeqProxy`, + * and transaction hash, then inserts the `(Key, tx)` pair into the map. + * Duplicate inserts (same transaction hash) are silently ignored by the + * underlying `std::map`. + * + * @param txn The signed transaction to enqueue. + */ void insert(std::shared_ptr const& txn); - // Pops the next transaction on account that follows seqProx in the - // sort order. Normally called when a transaction is successfully - // applied to the open ledger so the next transaction can be resubmitted - // without waiting for ledger close. - // - // The return value is often null, when an account has no more - // transactions. + /** Pops and returns the next eligible transaction for the same account. + * + * After `tx` has been successfully applied to the open ledger, call this + * method to retrieve and remove the immediately-following transaction for + * the same account, if one exists and is eligible. A transaction is + * eligible if it either: + * - uses a ticket (tickets may be applied regardless of sequence gaps), or + * - has a sequence number exactly one greater than `tx`'s sequence. + * + * The search uses `lower_bound` on a synthetic key whose `txId_` is + * `beast::zero` (which sorts before any real transaction ID) to locate the + * first map entry past `tx`'s position. If that entry belongs to a + * different account, or its sequence constraint is not satisfied, the + * method returns `nullptr`. + * + * @param tx The just-applied transaction whose account and sequence + * establish the search anchor. + * @return The next eligible transaction (removed from the set), or + * `nullptr` if no suitable successor exists. + */ std::shared_ptr popAcctTransaction(std::shared_ptr const& tx); + /** Resets the set for a new ledger round. + * + * Installs a fresh salt and clears all transactions, allowing the same + * `CanonicalTXSet` instance to be reused across rounds without + * reallocating the underlying container. + * + * @param salt New ledger hash to use as the account-key salt. + */ void reset(LedgerHash const& salt) { @@ -112,35 +199,54 @@ public: map_.clear(); } + /** Erases the transaction at `it` and returns an iterator to the next element. + * + * Supports in-place removal during iteration, as used by `applyTransactions()` + * in `BuildLedger.cpp` when a transaction succeeds or definitively fails. + * + * @param it A valid iterator into this set. + * @return Iterator to the element following the removed one. + */ const_iterator erase(const_iterator const& it) { return map_.erase(it); } + /** Returns an iterator to the first transaction in canonical order. */ [[nodiscard]] const_iterator begin() const { return map_.begin(); } + /** Returns a past-the-end iterator. */ [[nodiscard]] const_iterator end() const { return map_.end(); } + /** Returns the number of transactions currently in the set. */ [[nodiscard]] size_t size() const { return map_.size(); } + + /** Returns `true` if the set contains no transactions. */ [[nodiscard]] bool empty() const { return map_.empty(); } + /** Returns the salt hash that identifies this set's ordering context. + * + * Callers use this for logging the transaction set identity alongside + * the ledger close time (e.g., `RCLConsensus` logs `retriableTxs.key()` + * when building the canonical set from the consensus map). + */ [[nodiscard]] uint256 const& key() const { @@ -150,7 +256,9 @@ public: private: std::map> map_; - // Used to salt the accounts so people can't mine for low account numbers + // XORed into each account's sort key to prevent mining for low account + // numbers that would gain a persistent ordering advantage. Refreshed each + // ledger round via reset(). uint256 salt_; }; diff --git a/include/xrpl/ledger/Dir.h b/include/xrpl/ledger/Dir.h index cfbef357b1..483c6f0d80 100644 --- a/include/xrpl/ledger/Dir.h +++ b/include/xrpl/ledger/Dir.h @@ -5,18 +5,22 @@ namespace xrpl { -/** A class that simplifies iterating ledger directory pages - - The Dir class provides a forward iterator for walking through - the uint256 values contained in ledger directories. - - The Dir class also allows accelerated directory walking by - stepping directly from one page to the next using the next_page() - member function. - - As of July 2024, the Dir class is only being used with NFTokenOffer - directories and for unit tests. -*/ +/** Read-only range adaptor for a paged ledger directory (`ltDIR_NODE`). + * + * A ledger directory is a linked list of `DirectoryNode` SLEs, each holding + * a `STVector256` (`sfIndexes`) of 256-bit keys pointing to other ledger + * objects. `Dir` wraps that structure in a C++ forward-iterable range, + * hiding page-chasing and SLE loading behind `begin()`/`end()`. + * + * Construction reads the root page eagerly but loads no entry SLEs; + * per-entry loading is deferred to `operator*()`. The class is used + * with NFTokenOffer directories (`keylet::nft_buys()`, `keylet::nft_sells()`) + * and in unit tests with owner directories (`keylet::ownerDir()`). + * + * @note Callers that only need per-page counts (not per-entry SLEs) should + * use `nextPage()` as the loop increment and `pageSize()` for counting, + * which avoids the per-entry `ReadView::read()` calls entirely. + */ class Dir { private: @@ -27,17 +31,57 @@ private: public: class ConstIterator; + + /** `shared_ptr`, matching `ReadView::read()`'s return type. */ using value_type = std::shared_ptr; + /** Construct a range over the directory rooted at `key` in `view`. + * + * Reads the root `DirectoryNode` SLE immediately and caches its + * `sfIndexes`. If the root page is absent the range is empty. + * + * @param view The ledger view to read from; must outlive this object. + * @param key Keylet of the directory root page. + */ Dir(ReadView const&, Keylet const&); + /** Return an iterator to the first entry of the directory. + * + * If the root page is missing or its `sfIndexes` is empty, the returned + * iterator compares equal to `end()`. + * + * @return A `ConstIterator` positioned at the first directory entry, + * or `end()` if the directory is empty. + */ [[nodiscard]] ConstIterator begin() const; + /** Return a past-the-end sentinel iterator. + * + * The sentinel has `page_.key == root_.key` and `index_ == beast::zero`. + * An iterator reaches this state when `nextPage()` finds `sfIndexNext == 0` + * on the last `DirectoryNode` page. + * + * @return A past-the-end `ConstIterator`. + */ [[nodiscard]] ConstIterator end() const; }; +/** Forward iterator over entries in a paged ledger directory. + * + * Each dereference lazily loads the ledger object pointed to by the current + * directory entry key via `ReadView::read(keylet::child(index_))`. The result + * is cached in `cache_` and cleared on every advance, including page + * transitions. + * + * Equality compares `page_.key` and `index_`. Two iterators are equal when + * both fields match; comparing iterators from different views or roots is + * undefined (asserted in debug builds). + * + * @note Advancing an iterator that is already at `end()` is undefined. + * Always guard with `it != dir.end()` before incrementing. + */ class Dir::ConstIterator { public: @@ -47,42 +91,113 @@ public: using difference_type = std::ptrdiff_t; using iterator_category = std::forward_iterator_tag; + /** Return true if both iterators point to the same directory entry. + * + * Returns `false` if either view pointer is null. Asserts in debug builds + * that both iterators share the same view and root keylet. + * + * @param other The iterator to compare against. + * @return `true` if `page_.key` and `index_` match in both iterators. + */ bool operator==(ConstIterator const& other) const; + /** Return true if the iterators do not point to the same directory entry. + * + * @param other The iterator to compare against. + * @return `!(*this == other)`. + */ bool operator!=(ConstIterator const& other) const { return !(*this == other); } + /** Load and return the ledger object for the current directory entry. + * + * The result is cached after the first call and reused on subsequent + * dereferences of the same position. The cache is cleared on every + * advance (including page transitions). + * + * @return `shared_ptr` to the referenced ledger object, + * or `nullptr` if the object is not present in the view. + */ reference operator*() const; + /** Return a pointer to the current entry's `shared_ptr`. + * + * @return Pointer to the cached SLE shared pointer. + */ pointer operator->() const { return &**this; } + /** Advance to the next directory entry, crossing page boundaries as needed. + * + * When the end of the current page's `sfIndexes` is reached, calls + * `nextPage()` to load the subsequent `DirectoryNode`. If no next page + * exists the iterator converges to the `end()` sentinel. + * + * @return Reference to this iterator after advancement. + */ ConstIterator& operator++(); + /** Post-increment: return a copy of this iterator, then advance. + * + * @return Copy of the iterator before advancement. + */ ConstIterator operator++(int); + /** Jump directly to the first entry of the next `DirectoryNode` page. + * + * Reads `sfIndexNext` from the current page SLE. If the value is zero + * (last page), the iterator is set to the `end()` sentinel. Otherwise, + * loads `keylet::page(root_, sfIndexNext)` and positions the iterator + * at the beginning of that page's `sfIndexes`. + * + * This method is public so callers can skip an entire page without + * loading individual entries — useful when only the per-page count is + * needed (see `pageSize()`). + * + * @return Reference to this iterator, now positioned at the start of the + * next page, or at `end()` if the directory is exhausted. + */ ConstIterator& nextPage(); + /** Return the number of entries on the current page. + * + * Reports `sfIndexes.size()` for the currently loaded `DirectoryNode` + * without reading any entry SLEs. Combined with `nextPage()` as a loop + * increment, this enables O(pages) offer-count checks instead of + * O(entries). + * + * @return Number of `uint256` entries in the current page's `sfIndexes`. + */ std::size_t pageSize(); + /** Return the keylet of the currently loaded `DirectoryNode` page. + * + * @return `Keylet` identifying the current page SLE. + */ Keylet const& page() const { return page_; } + /** Return the `uint256` key of the current directory entry. + * + * Equal to `beast::zero` when the iterator is at `end()`. + * + * @return The current entry's 256-bit ledger object key. + */ uint256 index() const { diff --git a/include/xrpl/ledger/Ledger.h b/include/xrpl/ledger/Ledger.h index 2d6f48db2d..8b9ae6b4d9 100644 --- a/include/xrpl/ledger/Ledger.h +++ b/include/xrpl/ledger/Ledger.h @@ -1,3 +1,9 @@ +/** @file + * Declares the Ledger class — the central data structure of the XRP Ledger + * daemon — together with supporting types for genesis ledger construction + * and the CachedLedger alias. + */ + #pragma once #include @@ -20,44 +26,58 @@ class TransactionMaster; class SqliteStatement; +/** Tag type used to select the genesis-ledger constructor of Ledger. + * + * Pass the singleton `kCREATE_GENESIS` constant to construct ledger + * sequence 1. The explicit constructor prevents accidental conversions. + */ struct CreateGenesisT { explicit CreateGenesisT() = default; }; +/** Singleton tag constant passed to the genesis-ledger constructor. */ extern CreateGenesisT const kCREATE_GENESIS; -/** Holds a ledger. - - The ledger is composed of two SHAMaps. The state map holds all of the - ledger entries such as account roots and order books. The tx map holds - all of the transactions and associated metadata that made it into that - particular ledger. Most of the operations on a ledger are concerned - with the state map. - - This can hold just the header, a partial set of data, or the entire set - of data. It all depends on what is in the corresponding SHAMap entry. - Various functions are provided to populate or depopulate the caches that - the object holds references to. - - Ledgers are constructed as either mutable or immutable. - - 1) If you are the sole owner of a mutable ledger, you can do whatever you - want with no need for locks. - - 2) If you have an immutable ledger, you cannot ever change it, so no need - for locks. - - 3) Mutable ledgers cannot be shared. - - @note Presented to clients as ReadView - @note Calls virtuals in the constructor, so marked as final -*/ +/** Immutable or mutable snapshot of the XRP Ledger at a single sequence number. + * + * A Ledger owns two SHAMap Merkle–radix trees: `stateMap_` (all account + * state — account roots, trust lines, offers, escrows, amendments, fee + * settings, etc.) and `txMap_` (every transaction together with its + * execution metadata that produced this ledger's state). + * + * **Mutable/immutable lifecycle:** + * - A freshly constructed ledger begins mutable; it must not be shared + * across threads while mutable. + * - After `setImmutable()` is called the ledger hashes are finalised, + * both SHAMaps are locked, and the object may be shared freely without + * any locking. Any attempt to mutate the SHAMaps after this point will + * assert. + * - `setAccepted()` is the standard close-time + `setImmutable()` sequence + * used after consensus. + * + * The class inherits `DigestAwareReadView` (read + per-entry digest), + * `TxsRawView` (raw state and transaction mutation), and + * `CountedObject` (intrusive diagnostics). It is marked `final` + * because constructors call virtual functions through `setup()`. + * + * @note Presented to most callers through the `ReadView` interface. + * @note `txMap_` and `stateMap_` are declared `mutable` to allow + * `setFull()` and iterator operations in `const` contexts without + * compromising the logical-constness contract. + * @see CachedLedger — the standard shareable form used at rest. + */ class Ledger final : public std::enable_shared_from_this, public DigestAwareReadView, public TxsRawView, public CountedObject { public: + /** Copying and moving are prohibited. + * + * Ledger objects are always owned through `std::shared_ptr`. Shared + * ownership combined with the mutable-→-immutable transition makes + * value-semantic copies unsafe and unnecessary. + */ Ledger(Ledger const&) = delete; Ledger& operator=(Ledger const&) = delete; @@ -66,20 +86,22 @@ public: Ledger& operator=(Ledger&&) = delete; - /** Create the Genesis ledger. - - The Genesis ledger contains a single account whose - AccountID is generated with a Generator using the seed - computed from the string "masterpassphrase" and ordinal - zero. - - The account has an XRP balance equal to the total amount - of XRP in the system. No more XRP than the amount which - starts in this account can ever exist, with amounts - used to pay fees being destroyed. - - Amendments specified are enabled in the genesis ledger - */ + /** Construct ledger sequence 1 (the genesis ledger). + * + * Seeds a single master account whose `AccountID` is derived + * deterministically from the seed of `"masterpassphrase"`, credits it + * with `kINITIAL_XRP` drops, inserts the `sfAmendments` SLE for any + * pre-enabled amendments, and inserts the fee schedule SLE using either + * drop-native fields (`sfBaseFeeDrops`, etc.) when `featureXRPFees` is + * among `amendments`, or legacy integer fields otherwise. Ends with + * `setImmutable()`. + * + * @param rules Protocol rules in effect at genesis. + * @param fees Initial fee schedule (base fee, reserve, increment). + * @param amendments Amendments that are enabled from ledger 1 onward. + * Determines which fee-field format is used for the genesis fee SLE. + * @param family Node-store family that owns the SHAMap backing storage. + */ Ledger( CreateGenesisT, Rules rules, @@ -87,15 +109,37 @@ public: std::vector const& amendments, Family& family); + /** Construct an immutable header-only placeholder ledger. + * + * Creates SHAMaps initialised with the root hashes from `info` but does + * not attempt to fetch SHAMap nodes from the node store. The canonical + * ledger hash is computed immediately from the header fields. Used for + * skeleton or partial ledgers reconstructed from database metadata. + * + * @param info Fully populated ledger header (must include root hashes). + * @param rules Protocol rules in effect for this ledger. + * @param family Node-store family for the underlying SHAMaps. + */ Ledger(LedgerHeader const& info, Rules rules, Family& family); - /** Used for ledgers loaded from JSON files - - @param acquire If true, acquires the ledger if not found locally - - @note The fees parameter provides default values, but setup() may - override them from the ledger state if fee-related SLEs exist. - */ + /** Restore a ledger from its header, fetching SHAMap roots from the node store. + * + * Constructs both SHAMaps with the root hashes from `info` and calls + * `fetchRoot()` on each. If either root is absent from the node store, + * `loaded` is set to `false`; when `acquire` is also `true`, async + * acquisition is triggered via `family.missingNodeAcquireByHash()`. + * The resulting ledger is always immutable. + * + * @param info Ledger header, including `txHash` and `accountHash` roots. + * @param loaded Set to `false` on return if either SHAMap root was missing. + * @param acquire If `true`, trigger async node acquisition when `loaded` + * would be set to `false`. + * @param rules Protocol rules in effect for this ledger. + * @param fees Default fee values; `setup()` will override these from the + * on-ledger fee SLE if one exists. + * @param family Node-store family for the underlying SHAMaps. + * @param j Journal for missing-root warnings. + */ Ledger( LedgerHeader const& info, bool& loaded, @@ -105,15 +149,33 @@ public: Family& family, beast::Journal j); - /** Create a new ledger following a previous ledger - - The ledger will have the sequence number that - follows previous, and have - parentCloseTime == previous.closeTime. - */ + /** Create the next mutable ledger in the chain following `previous`. + * + * The new ledger has sequence `previous.seq() + 1`. Its `stateMap_` + * is a copy-on-write snapshot of `previous.stateMap_` so state changes + * do not affect the closed parent. Its `txMap_` starts empty (a fresh + * SHAMap for the new round's transactions). `parentCloseTime` is set + * to `previous.closeTime`; the close-time resolution is advanced via + * `getNextLedgerTimeResolution`. + * + * @param previous The preceding closed ledger; must be immutable. + * @param closeTime Proposed close time for the new ledger. + */ Ledger(Ledger const& previous, NetClock::time_point closeTime); - // used for database ledgers + /** Construct a mutable empty ledger for database reconstruction. + * + * Creates an empty, mutable ledger at `ledgerSeq` and calls `setup()` + * to initialise `fees_` and `rules_` from any state entries that may + * already exist. Used when the node store needs to rebuild a ledger + * from raw DB data outside the normal consensus flow. + * + * @param ledgerSeq Target ledger sequence number. + * @param closeTime Close time to record in the ledger header. + * @param rules Protocol rules for this ledger. + * @param fees Initial fee schedule (may be overridden by `setup()`). + * @param family Node-store family for the underlying SHAMaps. + */ Ledger( std::uint32_t ledgerSeq, NetClock::time_point closeTime, @@ -127,66 +189,118 @@ public: // ReadView // + /** Always returns `false`; Ledger objects are never open. */ bool open() const override { return false; } + /** Returns the ledger header (sequence, hashes, close time, drops, etc.). */ LedgerHeader const& header() const override { return header_; } + /** Overwrite the in-memory ledger header wholesale. + * + * Used during ledger reconstruction from external data before the + * ledger is made immutable. Do not call on an immutable ledger. + * + * @param info New header to install. + */ void setLedgerInfo(LedgerHeader const& info) { header_ = info; } + /** Returns the fee schedule parsed from the on-ledger fee SLE. */ Fees const& fees() const override { return fees_; } + /** Returns the protocol rules in effect for this ledger. */ Rules const& rules() const override { return rules_; } + /** Returns `true` if the state map contains an entry matching `k`. + * + * @param k Keylet identifying the ledger entry (type + key). + */ bool exists(Keylet const& k) const override; + /** Returns `true` if the state map contains an entry at the raw key. + * + * @param key 256-bit SHAMap key to look up (no type check). + */ bool exists(uint256 const& key) const; + /** Find the smallest state-map key strictly greater than `key`. + * + * @param key Lower bound (exclusive) for the search. + * @param last If set, keys >= `last` are not returned. + * @return The next key, or `std::nullopt` if none exists in range. + */ std::optional succ(uint256 const& key, std::optional const& last = std::nullopt) const override; + /** Deserialize and return the state entry identified by `k`. + * + * Checks the keylet type against the deserialized SLE; returns + * `nullptr` if the key is missing or the type check fails. + * + * @param k Keylet specifying the key and expected ledger-entry type. + * @return Shared pointer to the immutable SLE, or `nullptr`. + */ std::shared_ptr read(Keylet const& k) const override; + /** Return a begin iterator over all state-map entries. */ std::unique_ptr slesBegin() const override; + /** Return a past-the-end iterator over all state-map entries. */ std::unique_ptr slesEnd() const override; + /** Return an iterator to the first state-map entry with key > `key`. */ std::unique_ptr slesUpperBound(uint256 const& key) const override; + /** Return a begin iterator over all transaction-map entries. + * + * @note Transactions are yielded with metadata for closed ledgers and + * without metadata for open ledgers (always closed for `Ledger`). + */ std::unique_ptr txsBegin() const override; + /** Return a past-the-end iterator over all transaction-map entries. */ std::unique_ptr txsEnd() const override; + /** Returns `true` if the transaction map contains an entry for `key`. */ bool txExists(uint256 const& key) const override; + /** Deserialize and return the transaction (plus metadata) for `key`. + * + * For a closed ledger both the `STTx` and the `STObject` metadata are + * returned. Returns an empty pair if the key is not present. + * + * @param key Transaction ID to look up. + * @return Pair of `(STTx const*, STObject const*)` shared pointers; + * either or both may be null on miss. + */ tx_type txRead(key_type const& key) const override; @@ -194,6 +308,17 @@ public: // DigestAwareReadView // + /** Return the Merkle hash of the state-map leaf at `key`. + * + * Used by `CachedView` to detect whether a cached SLE is stale. + * Returns `std::nullopt` if no entry exists at `key`. + * + * @note The current implementation loads the SHAMap item from the node + * store as a side-effect; see the inline comment in `Ledger.cpp`. + * + * @param key 256-bit state-map key to hash. + * @return The leaf node hash, or `std::nullopt` if absent. + */ std::optional digest(key_type const& key) const override; @@ -201,18 +326,53 @@ public: // RawView // + /** Remove the state entry whose key matches `sle->key()`. + * + * Calls `logicError` if the key does not exist in the state map. + * + * @param sle Entry to remove; only the key is used. + */ void rawErase(std::shared_ptr const& sle) override; + /** Insert a new state entry for `sle`. + * + * Serializes the SLE and adds it to the state SHAMap. Calls + * `logicError` if an entry with the same key already exists. + * + * @param sle Entry to insert; must not already be present. + */ void rawInsert(std::shared_ptr const& sle) override; + /** Remove the state entry at the raw key `key`. + * + * Overload for callers that hold only the key rather than an SLE. + * Calls `logicError` if the key does not exist. + * + * @param key 256-bit state-map key of the entry to remove. + */ void rawErase(uint256 const& key); + /** Replace (overwrite) an existing state entry with `sle`. + * + * Serializes the SLE and updates the state SHAMap in place. Calls + * `logicError` if no entry exists at `sle->key()`. + * + * @param sle Replacement entry; key must already be present. + */ void rawReplace(std::shared_ptr const& sle) override; + /** Burn `fee` drops from the ledger's total XRP supply. + * + * Implements XRPL's deflationary model: transaction fees are + * permanently destroyed rather than redistributed. Decrements + * `header_.drops` directly. + * + * @param fee Amount to deduct from the total coin supply. + */ void rawDestroyXRP(XRPAmount const& fee) override { @@ -223,6 +383,17 @@ public: // TxsRawView // + /** Append a transaction + metadata blob to the transaction map. + * + * Encodes `txn` and `metaData` as two back-to-back variable-length + * fields and inserts the result at `key`. Asserts that `metaData` + * is non-null (open ledgers must not call this). Calls `logicError` + * if `key` is already present (duplicate transaction). + * + * @param key Transaction ID (SHAMap key). + * @param txn Serialized transaction blob. + * @param metaData Serialized transaction metadata blob; must be non-null. + */ void rawTxInsert( uint256 const& key, @@ -231,37 +402,66 @@ public: //-------------------------------------------------------------------------- + /** Mark this ledger as validated by the network. + * + * Sets `header_.validated = true`. This is a local-node annotation + * only; it does not affect the consensus hash or any on-ledger state. + */ void setValidated() const { header_.validated = true; } + /** Finalise timing fields and transition this ledger to immutable. + * + * Records `closeTime`, `closeResolution`, and the close-flag + * (`kS_LCF_NO_CONSENSUS_TIME` when `correctCloseTime` is `false`), + * then delegates to `setImmutable()`. + * + * @pre `!open()` — the ledger must already be closed. + * + * @param closeTime Agreed consensus close time. + * @param closeResolution Resolution used to bin the close time. + * @param correctCloseTime `true` if consensus agreed on the close time; + * `false` sets the no-consensus-time flag in the header. + */ void setAccepted( NetClock::time_point closeTime, NetClock::duration closeResolution, bool correctCloseTime); + /** Compute hashes and lock the ledger against further mutation. + * + * When `rehash` is `true` (the default): computes `header_.txHash` + * and `header_.accountHash` from the respective SHAMap roots, then + * computes the canonical ledger hash via `calculateLedgerHash()`. + * Regardless of `rehash`, sets `immutable_ = true`, calls + * `setImmutable()` on both SHAMaps, and calls `setup()` to populate + * `fees_` and `rules_` from the state map. + * + * @param rehash If `false`, skip hash computation (used when the + * hashes are already known, e.g. on load from the database). + */ void setImmutable(bool rehash = true); + /** Returns `true` if `setImmutable()` has been called on this ledger. */ bool isImmutable() const { return immutable_; } - /* Mark this ledger as "should be full". - - "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. - */ + /** Tell the node store to retain all SHAMap nodes for this ledger. + * + * "Full" is a local storage policy: when set, the node store will keep + * all state-map and transaction-map nodes for this ledger in durable + * storage rather than evicting them. Declared `const` because fullness + * is node-local metadata — two nodes holding the same ledger may differ + * on this property without affecting consensus. + */ void setFull() const { @@ -271,145 +471,283 @@ public: stateMap_.setLedgerSeq(header_.seq); } + /** Overwrite the total XRP supply recorded in the ledger header. + * + * Used when building ledgers from external data sources (e.g. JSON + * import) before the ledger is made immutable. + * + * @param totDrops New total supply in drops. + */ void setTotalDrops(std::uint64_t totDrops) { header_.drops = totDrops; } + /** Returns a read-only reference to the state SHAMap. */ SHAMap const& stateMap() const { return stateMap_; } + /** Returns a mutable reference to the state SHAMap. + * + * @note Only valid while the ledger is mutable. + */ SHAMap& stateMap() { return stateMap_; } + /** Returns a read-only reference to the transaction SHAMap. */ SHAMap const& txMap() const { return txMap_; } + /** Returns a mutable reference to the transaction SHAMap. + * + * @note Only valid while the ledger is mutable. + */ SHAMap& txMap() { return txMap_; } - // returns false on error + /** Serialize `sle` and add it directly to the state SHAMap. + * + * Convenience wrapper used during ledger construction from external + * data sources. Unlike `rawInsert`, this does not assert on failure. + * + * @param sle State entry to serialize and insert. + * @return `true` on success; `false` if the key already exists or the + * underlying `SHAMap::addItem` call fails. + */ bool addSLE(SLE const& sle); //-------------------------------------------------------------------------- + /** Update the two-tier skip list stored in the state map. + * + * The skip list enables O(1) historical hash lookup. This method + * maintains two SLEs: + * - `keylet::skip(prevIndex)` — a permanent record written for every + * 256-aligned predecessor sequence; stores up to 256 ancestor hashes. + * - `keylet::skip()` — a rolling window of the 256 most recent parent + * hashes; oldest entry is evicted when the list is full. + * + * Must be called on a mutable ledger before `setImmutable()`. + */ void updateSkipList(); + /** Verify that every SHAMap node for this ledger is reachable. + * + * Walks both the state map and the transaction map and collects missing + * node reports. Logs the first missing node of each type to `j`. + * + * @param j Journal to receive missing-node diagnostics. + * @param parallel If `true`, walks the state map using parallel + * traversal (faster on multi-core hardware). + * @return `true` if both maps are fully present; `false` if any nodes + * are missing. + */ bool walkLedger(beast::Journal j, bool parallel = false) const; + /** Perform basic sanity checks on the ledger header vs. SHAMap hashes. + * + * Verifies that `header_.hash`, `header_.accountHash`, and + * `header_.txHash` are all non-zero and that the account and + * transaction hashes match the actual SHAMap roots. + * + * @return `true` if all checks pass. + */ bool isSensible() const; + /** Assert internal SHAMap invariants for both the state and tx maps. + * + * Delegates to `SHAMap::invariants()` on each map. Intended for + * debug-build integrity checks. + */ void invariants() const; + + /** Release copy-on-write sharing of SHAMap nodes. + * + * After a copy-on-write snapshot is made (e.g. in the successor + * constructor), internal SHAMap nodes may be shared between the parent + * and child ledgers. Calling `unshare()` on the mutable child forces + * a deep copy so the two trees are fully independent. + */ void unshare() const; - /** - * get Negative UNL validators' master public keys + /** Read the current set of Negative UNL validators from the state map. * - * @return the public keys + * The Negative UNL is a consensus mechanism that temporarily removes + * chronically offline validators without breaking liveness. This + * method reads the `sfDisabledValidators` array from the + * `keylet::negativeUNL()` SLE. + * + * @return Master public keys of all currently disabled validators; + * empty if no Negative UNL entry exists or it has no members. */ hash_set negativeUNL() const; - /** - * get the to be disabled validator's master public key if any + /** Return the validator scheduled for disabling at the next flag ledger. * - * @return the public key if any + * Reads `sfValidatorToDisable` from the Negative UNL SLE, if present. + * + * @return The validator's master public key, or `std::nullopt` if none + * is pending. */ std::optional validatorToDisable() const; - /** - * get the to be re-enabled validator's master public key if any + /** Return the validator scheduled for re-enabling at the next flag ledger. * - * @return the public key if any + * Reads `sfValidatorToReEnable` from the Negative UNL SLE, if present. + * + * @return The validator's master public key, or `std::nullopt` if none + * is pending. */ std::optional validatorToReEnable() const; - /** - * update the Negative UNL ledger component. - * @note must be called at and only at flag ledgers - * must be called before applying UNLModify Tx + /** Apply the pending Negative UNL changes recorded in the state map. + * + * Promotes `sfValidatorToDisable` into `sfDisabledValidators` and + * removes `sfValidatorToReEnable` from that array. If the resulting + * disabled set is empty, the entire Negative UNL SLE is deleted. + * + * @note Must be called exactly once per flag ledger (sequence divisible + * by 256) and *before* any `UNLModify` transaction is applied. */ void updateNegativeUNL(); - /** Returns true if the ledger is a flag ledger */ + /** Returns `true` if this is a flag ledger (sequence divisible by 256). + * + * Flag ledgers carry out amendment votes, fee votes, and Negative UNL + * updates. These actions must not occur on non-flag ledgers. + */ bool isFlagLedger() const; - /** Returns true if the ledger directly precedes a flag ledger */ + /** Returns `true` if this ledger directly precedes a flag ledger. + * + * Voting ledgers (flagSeq − 1) are where validators cast their + * amendment and fee preferences before the flag-ledger processing pass. + */ bool isVotingLedger() const; + /** Deserialize and return a mutable SLE at keylet `k`. + * + * Unlike `read()`, the returned SLE is not `const` and may be passed + * to `rawReplace()` or `rawErase()`. Returns `nullptr` if the key + * is absent or the keylet type check fails. + * + * @note The caller must use the returned pointer only with the same + * `Ledger` instance; crossing to another view is a `LogicError`. + * + * @param k Keylet identifying the entry. + * @return Mutable SLE, or `nullptr` if not found. + */ std::shared_ptr peek(Keylet const& k) const; private: + /** SHAMap-backed iterator implementation for `ReadView::sles`. */ class SlesIterImpl; + + /** SHAMap-backed iterator implementation for `ReadView::txs`. + * + * Deserializes with metadata for closed ledgers, without for open ones. + */ class TxsIterImpl; + /** Populate `fees_` and `rules_` from the current state map. + * + * Reads `keylet::fees()` and applies the fee fields to `fees_`, then + * rebuilds `rules_` via `makeRulesGivenLedger`. Returns `false` if a + * `SHAMapMissingNode` is caught or if the fee SLE contains an illegal + * combination of old and new fee fields; otherwise returns `true`. + * + * @note Called by every constructor and by `setImmutable()`. + */ bool setup(); - /** @brief Deserialize a SHAMapItem containing a single STTx. + /** Deserialize a SHAMapItem containing a single `STTx`. * - * @param item The SHAMapItem to deserialize. - * @return A shared pointer to the deserialized transaction. - * @throw May throw on deserialization error. + * Used by `TxsIterImpl` for open ledgers (no metadata). + * + * @param item The SHAMap leaf to deserialize. + * @return Shared pointer to the deserialized transaction. + * @throw May throw on deserialization error. */ static std::shared_ptr deserializeTx(SHAMapItem const& item); - /** @brief Deserialize a SHAMapItem containing STTx + STObject metadata. + /** Deserialize a SHAMapItem containing an `STTx` followed by `STObject` metadata. * - * The SHAMapItem must contain two variable length serialization objects. + * The item must encode two back-to-back variable-length fields: the + * serialized transaction blob first, then the metadata blob. * - * @param item The SHAMapItem to deserialize. - * @return A pair containing shared pointers to the deserialized transaction - * and metadata. - * @throw May throw on deserialization error. + * @param item The SHAMap leaf to deserialize. + * @return Pair of shared pointers to the transaction and its metadata. + * @throw May throw on deserialization error. */ static std::pair, std::shared_ptr> deserializeTxPlusMeta(SHAMapItem const& item); + /** `true` after `setImmutable()` has been called; mutations are forbidden. */ bool immutable_; - // A SHAMap containing the transactions associated with this ledger. + /** Merkle–radix tree of transactions + metadata keyed by transaction ID. + * + * Declared `mutable` so `setFull()` and iterator accessors can be + * called in `const` contexts without violating logical immutability. + */ SHAMap mutable txMap_; - // A SHAMap containing the state objects for this ledger. + /** Merkle–radix tree of all ledger state entries (SLEs) keyed by their + * 256-bit key. + * + * Declared `mutable` for the same reason as `txMap_`. + */ SHAMap mutable stateMap_; - // Protects fee variables + /** Guards `fees_` during the narrow mutable window before `setImmutable()` + * completes; not held on the read path once the ledger is immutable. + */ std::mutex mutable mutex_; - Fees fees_; - Rules rules_; - LedgerHeader header_; - beast::Journal j_; + Fees fees_; /**< Fee schedule parsed from the on-ledger fee SLE. */ + Rules rules_; /**< Protocol rules derived from enabled amendments. */ + LedgerHeader header_; /**< Sequence, hashes, close time, coin supply, etc. */ + beast::Journal j_; /**< Journal for constructor and `setup()` diagnostics. */ }; -/** A ledger wrapped in a CachedView. */ +/** Standard shareable ledger type used at rest in most of the server. + * + * `CachedView` layers an `unordered_map` in front of the raw + * `Ledger`, caching deserialized SLEs by key so that frequently accessed + * state entries are not repeatedly deserialized from the SHAMap. This is + * the type that callers such as the transaction engine and RPC handlers + * typically hold, not a raw `Ledger`. + * + * @see CachedView + */ using CachedLedger = CachedView; } // namespace xrpl diff --git a/include/xrpl/ledger/LedgerTiming.h b/include/xrpl/ledger/LedgerTiming.h index dce28bd44b..ae176ed5bb 100644 --- a/include/xrpl/ledger/LedgerTiming.h +++ b/include/xrpl/ledger/LedgerTiming.h @@ -1,3 +1,15 @@ +/** @file + * Ledger close-time resolution binning and monotonicity enforcement. + * + * Provides compile-time constants and three header-only template functions + * that translate raw wall-clock observations into canonical, network-agreed + * close timestamps written into every immutable ledger record. The binning + * approach lets validators with imperfectly synchronized clocks converge on + * a single close time without requiring a global time source. + * + * @see getNextLedgerTimeResolution, roundCloseTime, effCloseTime + */ + #pragma once #include @@ -7,11 +19,18 @@ namespace xrpl { -/** Possible ledger close time resolutions. - - Values should not be duplicated. - @see getNextLedgerTimeResolution -*/ +/** Ordered ladder of candidate close-time bin sizes, in seconds. + * + * The six values — 10, 20, 30, 60, 90, 120 seconds — form a strictly + * increasing sequence. `getNextLedgerTimeResolution` traverses this array + * to coarsen (move toward index 5) on disagreement and to refine (move + * toward index 0) on agreement. The array order directly encodes the + * coarser/finer direction; no separate mapping is needed. + * + * Values must be unique and sorted in ascending order. + * + * @see getNextLedgerTimeResolution + */ std::chrono::seconds constexpr kLEDGER_POSSIBLE_TIME_RESOLUTIONS[] = { std::chrono::seconds{10}, std::chrono::seconds{20}, @@ -20,41 +39,77 @@ std::chrono::seconds constexpr kLEDGER_POSSIBLE_TIME_RESOLUTIONS[] = { std::chrono::seconds{90}, std::chrono::seconds{120}}; -//! Initial resolution of ledger close time. +/** Default close-time resolution used for all ordinary (non-genesis) ledgers. + * + * Equal to `kLEDGER_POSSIBLE_TIME_RESOLUTIONS[2]` (30 seconds). Every + * consensus round starts from this resolution and adjusts based on prior + * agreement history via `getNextLedgerTimeResolution`. + */ auto constexpr kLEDGER_DEFAULT_TIME_RESOLUTION = kLEDGER_POSSIBLE_TIME_RESOLUTIONS[2]; -//! Close time resolution in genesis ledger +/** Close-time resolution used exclusively for the genesis ledger. + * + * Equal to `kLEDGER_POSSIBLE_TIME_RESOLUTIONS[0]` (10 seconds), the finest + * available bin. There is no prior-ledger disagreement history at genesis, + * so the finest resolution is chosen as the starting point. + */ auto constexpr kLEDGER_GENESIS_TIME_RESOLUTION = kLEDGER_POSSIBLE_TIME_RESOLUTIONS[0]; -//! How often we increase the close time resolution (in numbers of ledgers) +/** Number of ledgers between successive close-time resolution refinements. + * + * When the prior ledger reached close-time consensus, the resolution moves + * one step finer only every 8th ledger. This conservative cadence avoids + * prematurely tightening the bin size after a brief period of agreement, + * which could immediately reintroduce disagreements on slightly skewed clocks. + * + * @see getNextLedgerTimeResolution, kDECREASE_LEDGER_TIME_RESOLUTION_EVERY + */ auto constexpr kINCREASE_LEDGER_TIME_RESOLUTION_EVERY = 8; -//! How often we decrease the close time resolution (in numbers of ledgers) +/** Number of ledgers between successive close-time resolution coarsenings. + * + * When the prior ledger failed to reach close-time consensus, the resolution + * moves one step coarser on every ledger (value = 1). This aggressive + * back-off quickly finds a bin size that absorbs the validators' clock skew, + * deliberately asymmetric with the slower refinement cadence. + * + * @see getNextLedgerTimeResolution, kINCREASE_LEDGER_TIME_RESOLUTION_EVERY + */ auto constexpr kDECREASE_LEDGER_TIME_RESOLUTION_EVERY = 1; -/** Calculates the close time resolution for the specified ledger. - - The XRPL protocol uses binning to represent time intervals using only one - timestamp. This allows servers to derive a common time for the next ledger, - without the need for perfectly synchronized clocks. - The time resolution (i.e. the size of the intervals) is adjusted dynamically - based on what happened in the last ledger, to try to avoid disagreements. - - @param previousResolution the resolution used for the prior ledger - @param previousAgree whether consensus agreed on the close time of the prior - ledger - @param ledgerSeq the sequence number of the new ledger - - @pre previousResolution must be a valid bin - from @ref kLEDGER_POSSIBLE_TIME_RESOLUTIONS - - @tparam Rep Type representing number of ticks in std::chrono::duration - @tparam Period An std::ratio representing tick period in - std::chrono::duration - @tparam Seq Unsigned integer-like type corresponding to the ledger sequence - number. It should be comparable to 0 and support modular - division. Built-in and tagged_integers are supported. -*/ +/** Compute the close-time resolution to use for the next ledger. + * + * Implements the adaptive binning policy: if the prior ledger failed to + * reach close-time consensus the bin size is coarsened (every ledger, + * per `kDECREASE_LEDGER_TIME_RESOLUTION_EVERY`); if it succeeded the bin + * size is refined (every 8th ledger, per + * `kINCREASE_LEDGER_TIME_RESOLUTION_EVERY`). Both adjustments saturate at + * the boundaries of `kLEDGER_POSSIBLE_TIME_RESOLUTIONS` rather than + * wrapping. The two rules are mutually exclusive — only one fires per call. + * + * Called by the consensus engine at the start of every round to set + * `closeResolution_`, which is then used for the full round's close-time + * voting and embedded in the accepted ledger. + * + * @param previousResolution The close-time resolution used for the prior + * ledger; must be one of the values in + * `kLEDGER_POSSIBLE_TIME_RESOLUTIONS`. + * @param previousAgree Whether the network agreed on the prior ledger's + * close time (true = finer bins are safe to try). + * @param ledgerSeq Sequence number of the ledger being built; must be + * non-zero. Used for the modulo-based rate-limiting of each direction. + * @return The resolution to apply for the new ledger, chosen from + * `kLEDGER_POSSIBLE_TIME_RESOLUTIONS`. + * + * @pre `previousResolution` is an element of `kLEDGER_POSSIBLE_TIME_RESOLUTIONS`. + * @pre `ledgerSeq != Seq{0}`. + * + * @tparam Rep Tick-count type of the `std::chrono::duration`. + * @tparam Period `std::ratio` tick period of the `std::chrono::duration`. + * @tparam Seq Unsigned integer-like type for the ledger sequence number; + * supports `operator%` and comparison with `Seq{0}`. Both built-in + * integers and XRPL `tagged_integer` wrappers are accepted. + */ template std::chrono::duration getNextLedgerTimeResolution( @@ -65,7 +120,6 @@ getNextLedgerTimeResolution( XRPL_ASSERT(ledgerSeq != Seq{0}, "xrpl::getNextLedgerTimeResolution : valid ledger sequence"); using namespace std::chrono; - // Find the current resolution: auto iter = std::find( std::begin(kLEDGER_POSSIBLE_TIME_RESOLUTIONS), std::end(kLEDGER_POSSIBLE_TIME_RESOLUTIONS), @@ -78,16 +132,12 @@ getNextLedgerTimeResolution( if (iter == std::end(kLEDGER_POSSIBLE_TIME_RESOLUTIONS)) return previousResolution; - // If we did not previously agree, we try to decrease the resolution to - // improve the chance that we will agree now. if (!previousAgree && (ledgerSeq % Seq{kDECREASE_LEDGER_TIME_RESOLUTION_EVERY} == Seq{0})) { if (++iter != std::end(kLEDGER_POSSIBLE_TIME_RESOLUTIONS)) return *iter; } - // If we previously agreed, we try to increase the resolution to determine - // if we can continue to agree. if (previousAgree && (ledgerSeq % Seq{kINCREASE_LEDGER_TIME_RESOLUTION_EVERY} == Seq{0})) { if (iter-- != std::begin(kLEDGER_POSSIBLE_TIME_RESOLUTIONS)) @@ -97,13 +147,26 @@ getNextLedgerTimeResolution( return previousResolution; } -/** Calculates the close time for a ledger, given a close time resolution. - - @param closeTime The time to be rounded - @param closeResolution The resolution - @return @b closeTime rounded to the nearest multiple of @b closeResolution. - Rounds up if @b closeTime is midway between multiples of @b closeResolution. -*/ +/** Round a ledger close time to the nearest bin boundary. + * + * Bins are aligned to multiples of `closeResolution` measured from the + * clock epoch (`time_since_epoch()`), so any two validators computing this + * on the same raw time will produce the same result regardless of local + * state — a correctness prerequisite for network agreement. Ties (a time + * exactly at the midpoint between two boundaries) round up to the later bin. + * + * A default-constructed `time_point{}` (the epoch sentinel signalling no + * agreed close time) is returned unchanged without any rounding. + * + * @param closeTime The raw close-time observation to round. + * @param closeResolution The bin size; must be positive and non-zero. + * @return `closeTime` rounded to the nearest epoch-anchored multiple of + * `closeResolution`, or `closeTime` unmodified if it equals + * `time_point{}`. + * + * @note Called by `effCloseTime` and also directly by the consensus engine + * via `asCloseTime()` to canonicalize individual peer proposals. + */ template std::chrono::time_point roundCloseTime( @@ -118,15 +181,30 @@ roundCloseTime( return closeTime - (closeTime.time_since_epoch() % closeResolution); } -/** Calculate the effective ledger close time - - After adjusting the ledger close time based on the current resolution, also - ensure it is sufficiently separated from the prior close time. - - @param closeTime The raw ledger close time - @param resolution The current close time resolution - @param priorCloseTime The close time of the prior ledger -*/ +/** Compute the effective close time for a ledger, enforcing monotonicity. + * + * Rounds `closeTime` via `roundCloseTime`, then clamps the result to be + * strictly greater than `priorCloseTime`. The clamp (`priorCloseTime + 1s`) + * handles the edge case where a very fast close would otherwise produce a + * rounded time equal to or earlier than the prior ledger's close time, + * violating the invariant that ledger timestamps increase strictly along the + * chain. When the rounded value is already later than `priorCloseTime`, it + * passes through unchanged. + * + * A default-constructed `closeTime` (the epoch sentinel for "no agreed close + * time") is returned unchanged without rounding or clamping. + * + * @param closeTime The raw close-time observation for this ledger. + * @param resolution The bin size for this round's close-time voting. + * @param priorCloseTime The effective close time of the preceding ledger; + * used as the strict lower bound. + * @return `max(roundCloseTime(closeTime, resolution), priorCloseTime + 1s)`, + * or `closeTime` unmodified if it equals `time_point{}`. + * + * @note Example edge cases (30 s bins, priorCloseTime = 0 s): + * - `effCloseTime(10s, 30s, 0s)` → `1s` (rounded = 0s, clamped to 1s) + * - `effCloseTime(16s, 30s, 0s)` → `30s` (rounded = 30s, passes through) + */ template std::chrono::time_point effCloseTime( diff --git a/include/xrpl/ledger/OpenView.h b/include/xrpl/ledger/OpenView.h index 59ab733211..7168de9721 100644 --- a/include/xrpl/ledger/OpenView.h +++ b/include/xrpl/ledger/OpenView.h @@ -14,21 +14,29 @@ namespace xrpl { -/** Open ledger construction tag. - - Views constructed with this tag will have the - rules of open ledgers applied during transaction - processing. +/** Tag type for constructing an open-ledger view. + * + * Pass `kOPEN_LEDGER` to the `OpenView` constructor to build a fresh open + * ledger on top of a base. The header sequence is incremented, `parentHash` + * and `parentCloseTime` are derived from the base, and `validated`/`accepted` + * flags are cleared. Rules are supplied explicitly by the caller. + * + * @see kOPEN_LEDGER */ inline constexpr struct OpenLedgerT { explicit constexpr OpenLedgerT() = default; } kOPEN_LEDGER{}; -/** Batch view construction tag. - - Views constructed with this tag are part of a stack of views - used during batch transaction applied. +/** Tag type for constructing a batch-mode view. + * + * Pass `kBATCH_VIEW` to the `OpenView` constructor when building a child view + * during batch transaction processing. The child wraps an existing `OpenView` + * and captures its current transaction count as `baseTxCount_`, so that + * `txCount()` ordinals remain globally unique and monotonically increasing + * within the enclosing ledger regardless of how many sub-views are stacked. + * + * @see kBATCH_VIEW */ inline constexpr struct BatchViewT { @@ -37,10 +45,31 @@ inline constexpr struct BatchViewT //------------------------------------------------------------------------------ -/** Writable ledger view that accumulates state and tx changes. - - @note Presented as ReadView to clients. -*/ +/** Mutable ledger view used during transaction processing. + * + * Implements the delta-accumulation pattern: holds an immutable base + * `ReadView` (typically the most recent closed ledger) and records all SLE + * mutations and inserted transactions as a pending diff on top of it. + * Nothing is written through to the base until `apply()` is called, making + * it safe to discard changes on failure. + * + * State-object mutations are buffered in `items_` (`RawStateTable`). All + * `ReadView` queries merge the base and the pending diff transparently, so + * the apparent ledger state is always consistent. Transaction records are + * kept in `txs_` (a PMR `std::map`); open ledgers omit metadata while + * closed representations include it. + * + * Both maps are backed by a 256 KB `monotonic_buffer_resource` for O(1) + * amortised allocation with no per-element heap overhead. The resource is + * a `unique_ptr` so move-construction maintains stable addressing for the + * maps' `polymorphic_allocator` raw pointers. + * + * @note Move assignment and copy assignment are deleted; only move + * construction and copy construction are available. + * @note Callers holding `ReadView const*` see a coherent read-only snapshot + * that merges base state and pending modifications without needing to + * know whether the ledger is settled. + */ class OpenView final : public ReadView, public TxsRawView { private: @@ -98,145 +127,249 @@ public: OpenView(OpenView&&) = default; - /** Construct a shallow copy. - - Effects: - - Creates a new object with a copy of - the modification state table. - - The objects managed by shared pointers are - not duplicated but shared between instances. - Since the SLEs are immutable, calls on the - RawView interface cannot break invariants. - */ + /** Construct a copy of this view with a fresh PMR arena. + * + * The modification state table (`items_`) and transaction map (`txs_`) + * are copied into a newly allocated 256 KB monotonic buffer. `shared_ptr` + * members (SLEs, `hold_`) are shared with the source — they are not + * deep-copied — which is safe because SLEs are immutable once published. + */ OpenView(OpenView const&); - /** Construct an open ledger view. - - Effects: - - The sequence number is set to the - sequence number of parent plus one. - - The parentCloseTime is set to the - closeTime of parent. - - If `hold` is not nullptr, retains - ownership of a copy of `hold` until - the MetaView is destroyed. - - Calls to rules() will return the - rules provided on construction. - - The tx list starts empty and will contain - all newly inserted tx. - */ + /** Construct a fresh open ledger view on top of a closed base. + * + * The header is derived from `base`: sequence is incremented by one, + * `parentCloseTime` is set to the base close time, `parentHash` is set + * to the base hash, and `validated`/`accepted` flags are cleared. + * The transaction list starts empty. + * + * @param base The most recent closed ledger; must outlive this view + * unless `hold` is provided. + * @param rules Rules governing this open ledger; may differ from what + * the base recorded. + * @param hold Optional shared pointer keeping `base`'s backing object + * alive for the lifetime of this view. + */ OpenView( OpenLedgerT, ReadView const* base, Rules rules, std::shared_ptr hold = nullptr); + /** Convenience overload that keeps the base alive via shared ownership. + * + * Equivalent to the three-argument `OpenLedgerT` constructor, but takes + * a `shared_ptr` so the caller need not manage lifetime separately. + * + * @param rules Rules governing this open ledger. + * @param base Shared pointer to the closed base ledger. + */ OpenView(OpenLedgerT, Rules const& rules, std::shared_ptr const& base) : OpenView(kOPEN_LEDGER, &*base, rules, base) { } + /** Construct a batch child view on top of an existing open ledger. + * + * Wraps `base` as a read-through fallback and snapshots its current + * `txCount()` into `baseTxCount_`. This ensures that `txCount()` on this + * child continues from where the parent left off, preserving monotonically + * increasing apply-ordinals in transaction metadata. + * + * @param base The parent `OpenView` to wrap; must outlive this child. + */ OpenView(BatchViewT, OpenView& base) : OpenView(std::addressof(base)) { baseTxCount_ = base.txCount(); } - /** Construct a new last closed ledger. - - Effects: - - The LedgerHeader is copied from the base. - - The rules are inherited from the base. - - The tx list starts empty and will contain - all newly inserted tx. - */ + /** Construct a view representing a last-closed ledger. + * + * Copies the `LedgerHeader` and `Rules` directly from `base`, and + * inherits its `open_` flag — so if the base was a closed ledger, this + * view will also report itself as closed. The transaction list starts + * empty. + * + * @param base The source ledger; must outlive this view unless `hold` + * is provided. + * @param hold Optional shared pointer keeping `base`'s backing object + * alive for the lifetime of this view. + */ OpenView(ReadView const* base, std::shared_ptr hold = nullptr); - /** Returns true if this reflects an open ledger. */ + /** Returns true if this view represents an open (not yet closed) ledger. */ bool open() const override { return open_; } - /** Return the number of tx inserted since creation. - - This is used to set the "apply ordinal" - when calculating transaction metadata. - */ + /** Return the total number of transactions applied since ledger construction. + * + * Computed as `baseTxCount_ + txs_.size()`. In batch mode `baseTxCount_` + * captures the parent view's count at the time this child was constructed, + * so ordinals are globally unique and monotonically increasing even when + * child views are committed incrementally. + * + * @return Number of transactions, used as the apply ordinal in metadata. + */ std::size_t txCount() const; - /** Apply changes. */ + /** Commit all accumulated changes to the target view. + * + * Replays every buffered SLE mutation (`items_`) into `to` via + * `RawStateTable::apply`, then iterates `txs_` and calls + * `to.rawTxInsert()` for each transaction. The typical call site is + * `ApplyViewImpl::apply()`, which applies a per-transaction sandbox into + * the enclosing `OpenView`; later the `OpenView` itself is applied into + * the final ledger object. + * + * @param to The target view that receives all mutations and transactions. + */ void apply(TxsRawView& to) const; // ReadView + /** @return The current ledger header (sequence, hashes, close times). */ LedgerHeader const& header() const override; + /** @return The fee schedule inherited from the base ledger. */ Fees const& fees() const override; + /** @return The amendment rules supplied at construction or inherited from base. */ Rules const& rules() const override; + /** Check whether a ledger entry exists, merging base state and pending diff. + * + * @param k Keylet identifying the entry. + * @return `true` if the entry exists in the merged view. + */ bool exists(Keylet const& k) const override; + /** Return the smallest key strictly greater than `key` in the merged view. + * + * @param key The lower bound (exclusive) to search from. + * @param last Optional upper bound (inclusive); search is bounded to + * `[key+1, last]` when provided. + * @return The next key, or `std::nullopt` if none exists in range. + */ std::optional succ(key_type const& key, std::optional const& last = std::nullopt) const override; + /** Read a ledger entry from the merged view (base + pending diff). + * + * @param k Keylet identifying the entry. + * @return Shared pointer to the immutable SLE, or `nullptr` if absent. + */ std::shared_ptr read(Keylet const& k) const override; + /** @return Iterator to the first SLE in the merged state map. */ std::unique_ptr slesBegin() const override; + /** @return Past-the-end iterator for the merged state map. */ std::unique_ptr slesEnd() const override; + /** @return Iterator to the first SLE whose key is > `key` in the merged map. + * + * @param key The exclusive lower bound. + */ std::unique_ptr slesUpperBound(uint256 const& key) const override; + /** @return Iterator to the first transaction in this view's tx map. + * + * @note For open ledgers the iterator will not deserialize metadata; + * for closed-ledger views it will. + */ std::unique_ptr txsBegin() const override; + /** @return Past-the-end iterator for this view's tx map. */ std::unique_ptr txsEnd() const override; + /** Check whether a transaction is present in this view's tx map. + * + * @param key The transaction ID. + * @return `true` if the transaction was inserted into this view. + */ bool txExists(key_type const& key) const override; + /** Read a transaction from this view, falling back to the base. + * + * @param key The transaction ID. + * @return Pair of `(STTx, optional metadata STObject)`; both pointers are + * null if the transaction is not found in this view or the base. + */ tx_type txRead(key_type const& key) const override; // RawView + /** Buffer a deletion of an existing state item. + * + * Delegates to `RawStateTable::erase`. The entry will be removed from + * the merged view immediately and will not appear in subsequent reads. + * + * @param sle The SLE to erase; its key is extracted from the object. + */ void rawErase(std::shared_ptr const& sle) override; + /** Buffer an insertion of a new state item. + * + * Delegates to `RawStateTable::insert`. The key must not already exist + * in the merged view. + * + * @param sle The new SLE to insert; its key is extracted from the object. + */ void rawInsert(std::shared_ptr const& sle) override; + /** Buffer a replacement of an existing state item. + * + * Delegates to `RawStateTable::replace`. The key must already exist in + * the merged view. + * + * @param sle The replacement SLE; its key is extracted from the object. + */ void rawReplace(std::shared_ptr const& sle) override; + /** Record destruction of XRP (burned as transaction fees). + * + * Delegates to `RawStateTable::destroyXRP`. The destroyed amount + * accumulates in the state table and is flushed to the target on `apply()`. + * + * @param fee The amount of XRP to destroy. + */ void rawDestroyXRP(XRPAmount const& fee) override; // TxsRawView + /** Record a transaction in this view's transaction map. + * + * For open ledgers `metaData` is typically `nullptr`; for closed-ledger + * representations it carries the serialized `TxMeta`. + * + * @param key The transaction ID (must be unique within this view). + * @param txn Serialized transaction blob. + * @param metaData Serialized transaction metadata, or `nullptr` for open + * ledger entries. + * @throws std::logic_error if `key` is already present in this view's + * tx map. Duplicate transaction IDs are a hard invariant violation. + */ void rawTxInsert( key_type const& key, diff --git a/include/xrpl/ledger/OrderBookDB.h b/include/xrpl/ledger/OrderBookDB.h index a0aee58e2a..0742a08bba 100644 --- a/include/xrpl/ledger/OrderBookDB.h +++ b/include/xrpl/ledger/OrderBookDB.h @@ -14,75 +14,122 @@ namespace xrpl { -/** Tracks order books in the ledger. - - This interface provides access to order book information, including: - - Which order books exist in the ledger - - Querying order books by issue - - Managing order book subscriptions - - The order book database is updated as ledgers are accepted and provides - efficient lookup of order book information for pathfinding and client - subscriptions. -*/ +/** Pure abstract index of all active order books across the ledger. + * + * An order book is a directed trading pair — a set of open `ltOFFER` entries + * sharing the same "taker pays" (`in`) and "taker gets" (`out`) assets. + * Because pathfinding and client subscriptions both need fast lookups of + * which markets exist, this index is maintained separately from ledger state. + * + * The interface lives in the public ledger layer; the concrete implementation + * (`OrderBookDBImpl`) is instantiated via `makeOrderBookDb()` and injected + * through the service registry, keeping heavy implementation details out of + * consumer headers. + * + * @note All internal maps are guarded by a `std::recursive_mutex`. The + * expensive full-ledger scan in `setup()` builds new maps outside the + * lock and swaps them in a brief critical section, so reader calls are + * only briefly blocked rather than held for the duration of a full ledger + * traversal. + */ class OrderBookDB { public: virtual ~OrderBookDB() = default; - /** Initialize or update the order book database with a new ledger. - - This method should be called when a new ledger is accepted to update - the order book database with the current state of all order books. - - @param ledger The ledger to scan for order books - */ + /** Notify the database that a new ledger has been accepted. + * + * Triggers a throttled full-ledger scan when needed. The scan is skipped + * if the new ledger is within 25,600 sequences ahead of the last scanned + * ledger (incremental updates from `processTxn` keep the index current) + * or within 16 sequences behind it (small reorg). Outside these windows + * a full scan is scheduled — synchronously in standalone mode, or as a + * background job queue task otherwise. The scan rebuilds the book maps in + * local variables then swaps them under a lock to minimise reader + * contention. + * + * @param ledger The accepted ledger to evaluate; the scan reads every + * `ltDIR_NODE` with an `sfExchangeRate` field and every `ltAMM` + * object to rebuild the in-memory book maps. + */ virtual void setup(std::shared_ptr const& ledger) = 0; - /** Add an order book to track. - - @param book The order book to add - */ + /** Register a single order book without triggering a full ledger scan. + * + * Used to record a newly discovered book incrementally — for example, + * when a new offer type is seen in `processTxn` before the next scheduled + * full `setup()` scan. + * + * @param book The directed trading pair to register. + */ virtual void addOrderBook(Book const& book) = 0; - /** Get all order books that want a specific issue. - - Returns a list of all order books where the taker pays the specified - issue. This is useful for pathfinding to find all possible next hops - from a given currency. - - @param asset The asset to search for - @param domain Optional domain restriction for the order book - @return Vector of books that want this issue - */ + /** Return all order books whose "taker pays" side is @p asset. + * + * The primary pathfinding query: given an asset a sender currently holds, + * enumerate every market where that asset can be spent. The pathfinding + * engine calls this at each hop to discover possible next steps toward + * the destination currency. + * + * @param asset The asset the taker pays (the "in" side of the book). + * @param domain If provided, restricts results to books scoped to that + * permissioned domain; if absent, returns only global books. + * @return All `Book` objects with @p asset as their `in` side. + */ virtual std::vector getBooksByTakerPays(Asset const& asset, std::optional const& domain = std::nullopt) = 0; - /** Get the count of order books that want a specific issue. - - @param asset The asset to search for - @param domain Optional domain restriction for the order book - @return Number of books that want this issue - */ + /** Return the number of distinct "taker gets" assets available for @p asset. + * + * Used as a breadth-limiting heuristic by the pathfinding engine: a large + * count signals a liquid hub currency; a small count may not warrant + * deeper exploration. + * + * @param asset The asset the taker pays. + * @param domain If provided, counts only books in that permissioned domain; + * if absent, counts only global books. + * @return The number of order books whose "in" side matches @p asset. + */ virtual int getBookSize(Asset const& asset, std::optional const& domain = std::nullopt) = 0; - /** Check if an order book to XRP exists for the given issue. - - @param asset The asset to check - @param domain Optional domain restriction for the order book - @return true if a book from this issue to XRP exists - */ + /** Return whether any order book exists that sells @p asset for XRP. + * + * The implementation maintains a dedicated O(1) set (`xrpBooks_` / + * `xrpDomainBooks_`) so this check does not scan `allBooks_`. Pathfinding + * uses it to identify assets that can be liquidated directly to XRP + * without an intermediate hop. + * + * @param asset The asset the taker pays. + * @param domain If provided, checks the permissioned-domain book set; + * if absent, checks the global book set. + * @return `true` if a book with @p asset as "in" and XRP as "out" exists. + */ virtual bool isBookToXRP(Asset const& asset, std::optional const& domain = std::nullopt) = 0; - /** - * Process a transaction for order book tracking. - * @param ledger The ledger the transaction was applied to - * @param alTx The transaction to process - * @param jvObj The JSON object of the transaction + /** Fan out a closed-ledger transaction to all relevant book subscribers. + * + * Walks the transaction's metadata nodes looking for `ltOFFER` entries + * that were created, modified, or deleted and extracts their `TakerGets` + * and `TakerPays` fields. For each affected offer, the reversed book + * (`TakerGets` → `TakerPays`) is looked up in the listeners map and, if + * subscribers exist, `BookListeners::publish()` is called. + * + * Deduplication is handled via a `hash_set havePublished` local + * to each call: a subscriber whose sequence number is already in the set + * will not receive a second copy of the same transaction, even if multiple + * of its subscribed books were touched. + * + * @note Only called for transactions with result `tesSUCCESS`. + * + * @param ledger The closed ledger the transaction was applied to. + * @param alTx The fully materialised transaction-in-ledger projection, + * including metadata. + * @param jvObj Version-indexed JSON representation of the transaction, + * built once upstream and dispatched to subscribers by API version. */ virtual void processTxn( @@ -90,18 +137,30 @@ public: AcceptedLedgerTx const& alTx, MultiApiJson const& jvObj) = 0; - /** - * Get the book listeners for a book. - * @param book The book to get the listeners for - * @return The book listeners for the book + /** Return the listener set for @p book, or `nullptr` if none exists. + * + * Used when unsubscribing: a `nullptr` result means no entry needs to be + * updated. Avoids creating empty `BookListeners` objects for every book + * that passes through the system. + * + * @param book The directed trading pair to look up. + * @return Shared pointer to the existing `BookListeners` for @p book, or + * `nullptr` if no subscribers are registered. */ virtual BookListeners::pointer getBookListeners(Book const&) = 0; - /** - * Create a new book listeners for a book. - * @param book The book to create the listeners for - * @return The new book listeners for the book + /** Return the listener set for @p book, creating it on demand. + * + * Used when subscribing: if no `BookListeners` entry exists for the book, + * one is created and inserted into the map before returning. + * + * @note Internally calls `getBookListeners()` under the same lock, + * which is why the implementation uses a `std::recursive_mutex`. + * + * @param book The directed trading pair to look up or create. + * @return Shared pointer to the (possibly newly created) `BookListeners` + * for @p book; never `nullptr`. */ virtual BookListeners::pointer makeBookListeners(Book const&) = 0; diff --git a/include/xrpl/ledger/PaymentSandbox.h b/include/xrpl/ledger/PaymentSandbox.h index 1cd89d9388..0fd07c6896 100644 --- a/include/xrpl/ledger/PaymentSandbox.h +++ b/include/xrpl/ledger/PaymentSandbox.h @@ -14,10 +14,35 @@ namespace detail { // VFALCO TODO Inline this implementation // into the PaymentSandbox class itself +/** Bookkeeping ledger for credits deferred during payment execution. + * + * Tracks every credit applied through a `PaymentSandbox` so that + * balance queries can subtract those credits before reporting available + * funds. This prevents circular-path liquidity: a credit arriving at an + * intermediate account mid-payment cannot be re-spent by an earlier step + * in the same path. + * + * Two separate tables are maintained: `creditsIOU_` for IOU trust-line + * transfers (keyed by canonical `(lowAccount, highAccount, currency)`) and + * `creditsMPT_` for MPT issuances (keyed by `MPTID`). Owner-count + * maximums are stored in `ownerCounts_`. + * + * @note This class is an implementation detail of `PaymentSandbox` and is + * not intended for direct use by other components. + */ class DeferredCredits { private: using KeyIOU = std::tuple; + + /** Per-trust-line record of accumulated debits and the pre-credit balance. + * + * Debits are split by canonical endpoint: `lowAcctDebits` accumulates + * amounts sent by the account whose `AccountID` is lower; `highAcctDebits` + * accumulates amounts sent by the other endpoint. `lowAcctOrigBalance` + * holds the low-account's balance at the moment the first credit was + * recorded; it is never overwritten by subsequent credits. + */ struct ValueIOU { explicit ValueIOU() = default; @@ -26,41 +51,52 @@ private: STAmount lowAcctOrigBalance; }; + /** Per-holder MPT debit record. + * + * `debit` accumulates the total MPT amount sent by this holder during + * the payment. `origBalance` is the holder's balance at the time the + * first debit was recorded; it is never overwritten by subsequent debits. + */ struct HolderValueMPT { HolderValueMPT() = default; - // Debit to issuer std::uint64_t debit = 0; std::uint64_t origBalance = 0; }; + /** Per-issuance MPT record aggregating credits and self-debits. + * + * `holders` tracks per-holder debit entries. `credit` accumulates the + * total amount issued (i.e. credited to holders) during the payment. + * `origBalance` holds the issuer's `OutstandingAmount` at the time the + * first entry was recorded; it is never overwritten. + * + * `selfDebit` handles the case where the MPT issuer owns a sell offer. + * Because the payment engine runs in reverse, crediting a holder first + * can transiently push `OutstandingAmount` above `MaximumAmount`. When + * the issuer's own sell offer is consumed in a later (reversed) step, + * the available issuance capacity must be reduced by the offer amount. + * `selfDebit` accumulates those offer amounts so that + * `balanceHookSelfIssueMPT` can correctly cap available issuance. + */ struct IssuerValueMPT { IssuerValueMPT() = default; std::map holders; - // Credit to holder std::uint64_t credit = 0; - // OutstandingAmount might overflow when MPTs are credited to a holder. - // Consider A1 paying 100MPT to A2 and A1 already having maximum MPTs. - // Since the payment engine executes a payment in revers, A2 is - // credited first and OutstandingAmount is going to be equal - // to MaximumAmount + 100MPT. In the next step A1 redeems 100MPT - // to the issuer and OutstandingAmount balances out. std::int64_t origBalance = 0; - // Self debit on offer selling MPT. Since the payment engine executes - // a payment in reverse, a crediting/buying step may overflow - // OutstandingAmount. A sell MPT offer owned by a holder can redeem any - // amount up to the offer's amount and holder's available funds, - // balancing out OutstandingAmount. But if the offer's owner is issuer - // then it issues more MPT. In this case the available amount to issue - // is the initial issuer's available amount less all offer sell amounts - // by the issuer. This is self-debit, where the offer's owner, - // issuer in this case, debits to self. std::uint64_t selfDebit = 0; }; using AdjustmentMPT = IssuerValueMPT; public: + /** Query result for a single IOU trust-line adjustment. + * + * Oriented from the perspective of the `main` account passed to + * `adjustmentsIOU()`: `debits` is what `main` has sent, `credits` is + * what `main` has received, and `origBalance` is `main`'s balance + * before the first credit in this sandbox was recorded. + */ struct AdjustmentIOU { AdjustmentIOU(STAmount d, STAmount c, STAmount b) @@ -72,14 +108,44 @@ public: STAmount origBalance; }; - // Get the adjustments for the balance between main and other. - // Returns the debits, credits and the original balance + /** Return the accumulated debit/credit adjustments for an IOU trust line. + * + * The result is oriented from `main`'s perspective: `debits` contains + * what `main` has sent to `other`, `credits` contains what `other` has + * sent to `main`, and `origBalance` is `main`'s balance at the time the + * first credit for this pair was recorded. + * + * @param main The account whose perspective determines orientation. + * @param other The counterparty account. + * @param currency The currency of the trust line. + * @return Adjustment record, or `std::nullopt` if no credits have been + * recorded for this pair in this sandbox. + */ [[nodiscard]] std::optional adjustmentsIOU(AccountID const& main, AccountID const& other, Currency const& currency) const; + /** Return the accumulated MPT adjustments for a given issuance. + * + * @param mptID The unique identifier of the MPT issuance. + * @return Adjustment record, or `std::nullopt` if no credits have been + * recorded for this issuance in this sandbox. + */ [[nodiscard]] std::optional adjustmentsMPT(MPTID const& mptID) const; + /** Record an IOU credit from `sender` to `receiver`. + * + * On the first call for a given `(sender, receiver, currency)` triple the + * pre-credit sender balance is saved as the original balance. Subsequent + * calls for the same triple accumulate debits without overwriting the + * original balance. + * + * @param sender Account sending the credit. + * @param receiver Account receiving the credit. + * @param amount Non-negative IOU amount being transferred. + * @param preCreditSenderBalance Sender's balance immediately before + * this credit is applied; only stored on the first call. + */ void creditIOU( AccountID const& sender, @@ -87,6 +153,20 @@ public: STAmount const& amount, STAmount const& preCreditSenderBalance); + /** Record an MPT credit from `sender` to `receiver`. + * + * Distinguishes between issuer-to-holder transfers (which increment the + * aggregate `credit` counter) and holder-to-issuer redemptions (which + * increment the per-holder `debit` counter). The original balances are + * stored only on the first call for each holder/issuance combination. + * + * @param sender Account sending the MPT. + * @param receiver Account receiving the MPT. + * @param amount Non-negative MPT amount being transferred. + * @param preCreditBalanceHolder Holder's MPT balance before this credit. + * @param preCreditBalanceIssuer Issuer's `OutstandingAmount` before this + * credit; only stored on the first call for this issuance. + */ void creditMPT( AccountID const& sender, @@ -95,22 +175,61 @@ public: std::uint64_t preCreditBalanceHolder, std::int64_t preCreditBalanceIssuer); + /** Record an MPT self-debit incurred by the issuer via a sell offer. + * + * When the issuer owns a sell offer and it is consumed, the payment + * engine (running in reverse) may have already credited a holder, + * pushing `OutstandingAmount` transiently above `MaximumAmount`. This + * call registers the offer amount as a self-debit so that + * `balanceHookSelfIssueMPT` can cap available issuance correctly. + * + * @param issue The MPT issuance involved. + * @param amount Amount of the issuer's sell offer that was consumed. + * @param origBalance Issuer's `OutstandingAmount` before this entry; only + * stored on the first call for this issuance. + */ void issuerSelfDebitMPT(MPTIssue const& issue, std::uint64_t amount, std::int64_t origBalance); + /** Record an owner-count transition for `account`. + * + * Stores the maximum of `cur` and `next`, and takes the maximum with any + * previously recorded value. Because payments only ever decrease owner + * counts, the highest observed count is the conservative bound that + * prevents a transient low count from bypassing reserve checks mid-payment. + * + * @param id Account whose owner count is changing. + * @param cur Current owner count before the transition. + * @param next Owner count after the transition. + */ void ownerCount(AccountID const& id, std::uint32_t cur, std::uint32_t next); - // Get the adjusted owner count. Since DeferredCredits is meant to be used - // in payments, and payments only decrease owner counts, return the max - // remembered owner count. + /** Return the maximum owner count observed for `account` in this sandbox. + * + * Since payments only decrease owner counts, the maximum is the correct + * conservative bound for reserve checks. + * + * @param id Account to query. + * @return The peak owner count, or `std::nullopt` if no transition has + * been recorded for this account. + */ [[nodiscard]] std::optional ownerCount(AccountID const& id) const; + /** Merge this sandbox's deferred credits into a parent sandbox. + * + * Debit accumulators and self-debit fields are summed; original balances + * are never overwritten (the parent's earlier record takes precedence). + * Owner-count maximums are taken across both sandboxes. + * + * @param to The parent `DeferredCredits` table to merge into. + */ void apply(DeferredCredits& to); private: + /** Produce a canonical `KeyIOU` by ordering the two accounts. */ static KeyIOU makeKeyIOU(AccountID const& a1, AccountID const& a2, Currency const& currency); @@ -123,18 +242,29 @@ private: //------------------------------------------------------------------------------ -/** A wrapper which makes credits unavailable to balances. - - This is used for payments and pathfinding, so that consuming - liquidity from a path never causes portions of that path or - other paths to gain liquidity. - - The behavior of certain free functions in the ApplyView API - will change via the balanceHook and creditHook overrides - of PaymentSandbox. - - @note Presented as ApplyView to clients -*/ +/** Speculative ledger view that hides in-flight credits from balance queries. + * + * The XRPL payment engine processes multi-hop paths where value flows through + * chains of trust lines, order books, and AMM pools. Without a guard, a + * credit arriving at an intermediate account mid-path could immediately + * appear as spendable liquidity for a later step in the same path — allowing + * phantom value to be created. `PaymentSandbox` prevents this by intercepting + * every credit via the hook protocol defined in `ApplyView` and recording it + * in a `DeferredCredits` table. Balance queries then subtract those deferred + * credits so freshly-received funds are invisible to outgoing transfer checks + * until the entire transaction commits. + * + * `PaymentSandbox` can be stacked: constructing one on top of another via the + * pointer constructors creates a child sandbox whose deferred credits chain to + * the parent. The pathfinding engine uses this to evaluate each candidate + * strand in a disposable child, committing to the parent only on success. + * + * @note When constructing on top of an existing `PaymentSandbox`, you **must** + * use the explicit pointer constructors. Using the plain `ApplyView*` + * constructor would bypass deferred-credit propagation and break invariants. + * + * @note Presented as `ApplyView` to clients. + */ class PaymentSandbox final : public detail::ApplyViewBase { public: @@ -147,27 +277,40 @@ public: PaymentSandbox(PaymentSandbox&&) = default; + /** Construct a root payment sandbox over a read-only base view. + * + * @param base The underlying ledger state to layer mutations on top of. + * @param flags Transaction-processing flags forwarded to `ApplyViewBase`. + */ PaymentSandbox(ReadView const* base, ApplyFlags flags) : ApplyViewBase(base, flags) { } + /** Construct a payment sandbox over an existing `ApplyView`. + * + * Inherits the flags of the base view. Use the explicit pointer + * constructors instead if `base` is itself a `PaymentSandbox`. + * + * @param base The mutable view to build on top of. + */ PaymentSandbox(ApplyView const* base) : ApplyViewBase(base, base->flags()) { } - /** Construct on top of existing PaymentSandbox. - - The changes are pushed to the parent when - apply() is called. - - @param parent A non-null pointer to the parent. - - @note A pointer is used to prevent confusion - with copy construction. - */ - // VFALCO If we are constructing on top of a PaymentSandbox, - // or a PaymentSandbox-derived class, we MUST go through - // one of these constructors or invariants will be broken. + /** Construct a child payment sandbox on top of an existing `PaymentSandbox`. + * + * The child's deferred-credit table chains to the parent so that balance + * adjustments aggregate correctly across the sandbox stack. Changes are + * not visible in the parent until `apply(PaymentSandbox&)` is called. + * + * @param parent Non-null pointer to the parent sandbox. A pointer is + * used rather than a reference to prevent confusion with copy + * construction. + * + * @note This overload set **must** be used whenever building on top of + * a `PaymentSandbox` or derived class. The plain `ApplyView*` + * constructor does not propagate deferred credits. + */ /** @{ */ explicit PaymentSandbox(PaymentSandbox const* base) : ApplyViewBase(base, base->flags()), ps_(base) @@ -179,17 +322,67 @@ public: } /** @} */ + /** Return the IOU balance adjusted for deferred credits. + * + * Walks the sandbox chain (this → parent → … ) and accumulates total + * debits from all ancestor tables. Returns + * `min(amount, origBalance - totalDebits, minObservedBalance)` to + * handle edge cases where rounding in the deferred table could otherwise + * overestimate usable funds. A computed negative XRP result is clamped + * to zero (it is not an error — it arises when a large credit is + * followed by the same debit within the path). + * + * @param account The account whose perspective determines orientation. + * @param issuer The IOU issuer (doubles as the currency issuer). + * @param amount The raw balance as reported by the underlying ledger. + * @return Adjusted balance with deferred credits hidden. + */ [[nodiscard]] STAmount balanceHookIOU(AccountID const& account, AccountID const& issuer, STAmount const& amount) const override; + /** Return the MPT holder or issuer balance adjusted for deferred credits. + * + * Walks the sandbox chain accumulating per-holder debits (if `account` + * is a holder) or the aggregate issuer credit (if `account` is the + * issuer). Returns `min(amount, origBalance - totalAdjustment, + * minObservedBalance)`, clamped to zero. + * + * @param account The account being queried (holder or issuer). + * @param issue The MPT issuance. + * @param amount The raw balance as reported by the underlying ledger. + * @return Adjusted balance with deferred credits hidden. + */ [[nodiscard]] STAmount balanceHookMPT(AccountID const& account, MPTIssue const& issue, std::int64_t amount) const override; + /** Return the issuer's available MPT issuance capacity, net of self-debits. + * + * When the issuer owns sell offers and the payment engine (running in + * reverse) has already consumed some of them, those amounts are recorded + * as self-debits. This hook caps available issuance at + * `origOutstandingAmount - totalSelfDebits`, returning zero if the result + * is non-positive. + * + * @param issue The MPT issuance. + * @param amount The raw `OutstandingAmount` from the underlying ledger. + * @return Available issuance capacity after subtracting self-debits. + */ [[nodiscard]] STAmount balanceHookSelfIssueMPT(MPTIssue const& issue, std::int64_t amount) const override; + /** Record an IOU credit in the deferred-credits table. + * + * Called by ledger mutation helpers at every IOU transfer. The recorded + * debit is used by `balanceHookIOU` to hide this credit from future + * balance queries within the same payment path. + * + * @param from Account sending the credit. + * @param to Account receiving the credit. + * @param amount Non-negative IOU amount being transferred. + * @param preCreditBalance Sender's balance immediately before this credit. + */ void creditHookIOU( AccountID const& from, @@ -197,6 +390,19 @@ public: STAmount const& amount, STAmount const& preCreditBalance) override; + /** Record an MPT credit in the deferred-credits table. + * + * Called by ledger mutation helpers at every MPT transfer. The recorded + * debit is used by `balanceHookMPT` to hide this credit from future + * balance queries within the same payment path. + * + * @param from Account sending the MPT. + * @param to Account receiving the MPT. + * @param amount Non-negative MPT amount being transferred. + * @param preCreditBalanceHolder Holder's MPT balance before this credit. + * @param preCreditBalanceIssuer Issuer's `OutstandingAmount` before this + * credit. + */ void creditHookMPT( AccountID const& from, @@ -205,22 +411,60 @@ public: std::uint64_t preCreditBalanceHolder, std::int64_t preCreditBalanceIssuer) override; + /** Record an MPT issuer self-debit arising from a consumed sell offer. + * + * Called when the MPT issuer's own sell offer is consumed during + * payment processing. Accumulates the offer amount in the + * `DeferredCredits` self-debit field so that `balanceHookSelfIssueMPT` + * can correctly limit further issuance capacity. + * + * @param issue The MPT issuance. + * @param amount Amount consumed from the issuer's sell offer. + * @param origBalance Issuer's `OutstandingAmount` before this entry. + */ void issuerSelfDebitHookMPT(MPTIssue const& issue, std::uint64_t amount, std::int64_t origBalance) override; + /** Record an owner-count transition for reserve-check purposes. + * + * Stores the maximum of `cur` and `next` in the deferred-credits table. + * Because payments only decrease owner counts, the peak value is the + * conservative bound that prevents a transient low count from bypassing + * reserve checks mid-payment. + * + * @param account Account whose owner count is changing. + * @param cur Owner count before the transition. + * @param next Owner count after the transition. + */ void adjustOwnerCountHook(AccountID const& account, std::uint32_t cur, std::uint32_t next) override; + /** Return the peak owner count observed for `account` in this sandbox chain. + * + * Walks the sandbox chain and returns the maximum recorded count across + * all ancestors, or `count` if no transition has been recorded. + * + * @param account Account to query. + * @param count Baseline count from the underlying ledger. + * @return The peak owner count seen across the sandbox chain. + */ [[nodiscard]] std::uint32_t ownerCountHook(AccountID const& account, std::uint32_t count) const override; - /** Apply changes to base view. - - `to` must contain contents identical to the parent - view passed upon construction, else undefined - behavior will result. - */ + /** Commit changes to a base view. + * + * The two overloads serve different commit targets: + * - `apply(RawView&)` is the terminal commit: asserts this sandbox has + * no parent (`ps_ == nullptr`) and flushes the state journal to the + * raw ledger. The `RawView` must contain state identical to the view + * passed at construction, otherwise behavior is undefined. + * - `apply(PaymentSandbox&)` asserts that `&to == ps_` (you can only + * apply to your direct parent) and propagates both the state journal + * and the deferred-credits table into the parent sandbox. + * + * @param to The target view to flush changes into. + */ /** @{ */ void apply(RawView& to); @@ -229,6 +473,10 @@ public: apply(PaymentSandbox& to); /** @} */ + /** Return the amount of XRP destroyed (as fees) during this payment. + * + * Delegates to `items_.dropsDestroyed()`. Distinct from transferred XRP. + */ [[nodiscard]] XRPAmount xrpDestroyed() const; diff --git a/include/xrpl/ledger/PendingSaves.h b/include/xrpl/ledger/PendingSaves.h index a18292df68..07e40b31bf 100644 --- a/include/xrpl/ledger/PendingSaves.h +++ b/include/xrpl/ledger/PendingSaves.h @@ -8,12 +8,49 @@ namespace xrpl { -/** Keeps track of which ledgers haven't been fully saved. - - During the ledger building process this collection will keep - track of those ledgers that are being built but have not yet - been completely written. -*/ +/** Coordination primitive tracking validated ledgers not yet fully written to + * the SQLite relational database. + * + * When a validated ledger is being persisted, there is a window in which it + * exists in memory but its index entries are incomplete on disk. Any code that + * reports the "validated range" of ledgers to peers or clients must exclude + * these in-progress sequences; otherwise it could direct a requester to query + * a partially-written row. + * + * ## Internal state machine + * + * The internal map encodes three observable states per ledger sequence: + * + * | Map state | Meaning | + * |----------------------------|--------------------------------------------| + * | key absent | Not pending; safe for DB queries | + * | key present, value `false` | Registered/dispatched, write not started | + * | key present, value `true` | A thread is actively writing to SQLite | + * + * The canonical "finished" state is key-absent; `finishWork()` erases the + * entry (rather than resetting the flag) so that `pending()` and the blocking + * loop in `shouldWork()` use absence as the termination condition. + * + * ## Typical call sequence + * + * 1. `pendSaveValidated()` calls `shouldWork(seq, isSynchronous)` to either + * claim a fresh entry or "steal" a registered-but-unstarted one. + * 2. `saveValidatedLedger()` calls `startWork(seq)` to atomically flip the + * flag from `false` → `true`. A `false` return means another thread won + * the race; the caller logs "Save aborted" and exits early. + * 3. `saveValidatedLedger()` calls `finishWork(seq)` after the DB write + * completes, waking any synchronous waiters. + * 4. `LedgerMaster::getValidatedRange()` calls `getSnapshot()` to trim the + * reported min/max validated range, excluding any in-progress sequences. + * + * This class is a pure coordination primitive. It does not own a thread pool + * or `JobQueue`; all scheduling policy lives in `pendSaveValidated()`. + * + * @note Thread-safe. All methods acquire `mutex_` internally. The synchronous + * blocking path in `shouldWork()` re-acquires the lock after each + * `await_.wait()` and re-checks in a loop because `notify_all()` can + * wake multiple waiters simultaneously. + */ class PendingSaves { private: @@ -22,12 +59,18 @@ private: std::condition_variable await_; public: - /** Start working on a ledger - - This is called prior to updating the SQLite indexes. - - @return 'true' if work should be done - */ + /** Atomically claim the right to begin writing a ledger to the database. + * + * Flips the map entry for @p seq from `false` to `true`, signalling that + * a thread is actively writing to SQLite. This must be called after + * `shouldWork()` returns `true` and before the DB write begins. + * + * @param seq Ledger sequence number to claim. + * @return `true` if this caller successfully claimed the write; `false` if + * the entry is absent (write already completed) or already `true` + * (another thread started it first). A `false` return is the caller's + * signal to abort with a "Save aborted" log and return early. + */ bool startWork(LedgerIndex seq) { @@ -45,12 +88,14 @@ public: return true; } - /** Finish working on a ledger - - This is called after updating the SQLite indexes. - The tracking of the work in progress is removed and - threads awaiting completion are notified. - */ + /** Mark a ledger's database write as complete and wake any waiters. + * + * Erases the entry for @p seq from the map — key-absent is the canonical + * "done" state — then calls `notify_all()` so any synchronous caller + * blocked in `shouldWork()` can re-evaluate. + * + * @param seq Ledger sequence number whose write has completed. + */ void finishWork(LedgerIndex seq) { @@ -60,7 +105,14 @@ public: await_.notify_all(); } - /** Return `true` if a ledger is in the progress of being saved. */ + /** Return `true` if @p seq has a pending or in-progress database write. + * + * A `true` result means the sequence appears in the map (either + * dispatched-but-not-started or actively writing). Callers use this to + * avoid re-dispatching a save that is already in flight. + * + * @param seq Ledger sequence number to test. + */ bool pending(LedgerIndex seq) { @@ -68,14 +120,34 @@ public: return map_.contains(seq); } - /** Check if a ledger should be dispatched - - Called to determine whether work should be done or - dispatched. If work is already in progress and the - call is synchronous, wait for work to be completed. - - @return 'true' if work should be done or dispatched - */ + /** Determine whether the caller should proceed with (or wait for) a save. + * + * This is the entry point for `pendSaveValidated()`. It implements the + * full dispatch/steal/wait decision: + * + * - **Not present**: Inserts `(seq, false)` and returns `true` — the + * caller owns the work. + * - **Present as `false`** (registered, unstarted): + * - Asynchronous caller: returns `false` (already dispatched; skip). + * - Synchronous caller: returns `true`, stealing the work before any + * thread can claim it via `startWork()`. + * - **Present as `true`** (write in progress): + * - Asynchronous caller: unreachable in practice; the `!isSynchronous` + * branch returns `false` before reaching the wait. + * - Synchronous caller: blocks on `await_` in a `do/while` loop, + * re-checking after each `notify_all()` from `finishWork()`, until + * the entry disappears (write complete). + * + * @param seq Ledger sequence number to check or register. + * @param isSynchronous `true` if the caller requires the write to be + * complete before returning; `false` if dispatch-once is sufficient. + * @return `true` if the caller should perform (or has stolen) the write; + * `false` if the work is already dispatched or complete. + * + * @note The blocking synchronous path re-acquires `mutex_` after each + * wake-up and loops because `notify_all()` may unblock multiple + * waiters; only one will find the entry absent. + */ bool shouldWork(LedgerIndex seq, bool isSynchronous) { @@ -108,12 +180,20 @@ public: } while (true); } - /** Get a snapshot of the pending saves - - Each entry in the returned map corresponds to a ledger - that is in progress or dispatched. The boolean indicates - whether work is currently in progress. - */ + /** Return a point-in-time copy of the pending-saves map. + * + * Used by `LedgerMaster::getValidatedRange()` to trim the reported + * min/max validated-ledger range: any sequence present in the snapshot — + * regardless of whether its flag is `false` (dispatched) or `true` + * (writing) — is excluded from the range to avoid directing peers to + * query a partially-written DB row. + * + * The returned map is a value copy taken under `mutex_`; the caller may + * iterate it freely without holding any lock. + * + * @return A snapshot of `map_`, where each key is an in-flight ledger + * sequence and each value is `false` (unstarted) or `true` (active). + */ std::map getSnapshot() const { diff --git a/include/xrpl/ledger/RawView.h b/include/xrpl/ledger/RawView.h index cfcf807e13..a73ed18859 100644 --- a/include/xrpl/ledger/RawView.h +++ b/include/xrpl/ledger/RawView.h @@ -6,10 +6,29 @@ namespace xrpl { -/** Interface for ledger entry changes. - - Subclasses allow raw modification of ledger entries. -*/ +/** Low-level write surface for committing ledger state mutations. + * + * Defines the three-operation contract (`rawErase`, `rawInsert`, + * `rawReplace`) plus an XRP-burn hook (`rawDestroyXRP`) that together + * represent the minimal interface a backing store must provide to absorb + * flushed changes from a sandbox. + * + * `detail::RawStateTable::apply(RawView&)` is the canonical driver: + * it iterates its buffered erase/insert/replace actions and dispatches + * each through the corresponding method here, so flushing logic is written + * once and any concrete target — a finalising `Ledger`, an `OpenView`, or + * another sandbox — implements the contract without exposing checkout + * semantics. + * + * The "raw" prefix is a semantic contract: these methods perform no + * precondition checking, no journaling, and no ownership tracking. + * They are the trusted commit surface, not the API that transaction + * logic should call directly. + * + * @note The copy constructor is defaulted (subclasses may need to snapshot + * state), but copy assignment is deleted to prevent silent cross-type + * assignment through the base interface. + */ class RawView { public: @@ -19,66 +38,79 @@ public: RawView& operator=(RawView const&) = delete; - /** Delete an existing state item. - - The SLE is provided so the implementation - can calculate metadata. - */ + /** Unconditionally remove an existing state entry. + * + * The full SLE (not just its key) is passed so that implementations + * can compute metadata such as changes to owner count or the type of + * the deleted object. + * + * @param sle The ledger entry to remove. The key is derived from + * the SLE itself; the entry must exist in the backing store. + */ virtual void rawErase(std::shared_ptr const& sle) = 0; - /** Unconditionally insert a state item. - - Requirements: - The key must not already exist. - - Effects: - - The key is associated with the SLE. - - @note The key is taken from the SLE - */ + /** Unconditionally insert a new state entry. + * + * The key is read from the SLE rather than passed separately, + * which prevents key/value mismatches at the call site. + * + * @param sle The ledger entry to insert. The key must not already + * exist in the backing store. + */ virtual void rawInsert(std::shared_ptr const& sle) = 0; - /** Unconditionally replace a state item. - - Requirements: - - The key must exist. - - Effects: - - The key is associated with the SLE. - - @note The key is taken from the SLE - */ + /** Unconditionally overwrite an existing state entry. + * + * The key is read from the SLE rather than passed separately, + * which prevents key/value mismatches at the call site. + * + * @param sle The replacement ledger entry. The key must already + * exist in the backing store. + */ virtual void rawReplace(std::shared_ptr const& sle) = 0; - /** Destroy XRP. - - This is used to pay for transaction fees. - */ + /** Permanently remove XRP drops from the ledger supply. + * + * XRPL burns transaction fees rather than redistributing them. + * This method is the accounting hook for that burn: separating it + * from `rawErase` keeps fee accounting explicit and auditable. + * + * @param fee The quantity of XRP drops to destroy. + */ virtual void rawDestroyXRP(XRPAmount const& fee) = 0; }; //------------------------------------------------------------------------------ -/** Interface for changing ledger entries with transactions. - - Allows raw modification of ledger entries and insertion - of transactions into the transaction map. -*/ +/** Extends `RawView` with the ability to insert transactions into the + * ledger's transaction map. + * + * The split between `RawView` (state-only writes) and `TxsRawView` + * (state plus transaction map) is architecturally significant. + * `detail::ApplyViewBase` — the sandbox used during transaction + * processing — only needs `RawView`: sandboxes accumulate state + * mutations but do not independently maintain a transaction map. + * `OpenView`, by contrast, inherits both `ReadView` and `TxsRawView` + * because it is the accumulation point for an open ledger round and + * must track both the growing state diff and the applied-transaction + * set. + */ class TxsRawView : public RawView { public: - /** Add a transaction to the tx map. - - Closed ledgers must have metadata, - while open ledgers omit metadata. - */ + /** Insert a serialized transaction into the ledger's transaction map. + * + * @param key The transaction's map key (typically its hash). + * @param txn Serialized transaction blob; must not be null. + * @param metaData Serialized transaction metadata, or null for open + * ledgers. Closed ledgers must supply metadata; open ledgers must + * pass null because consensus has not yet produced execution + * results. + */ virtual void rawTxInsert( ReadView::key_type const& key, diff --git a/include/xrpl/ledger/ReadView.h b/include/xrpl/ledger/ReadView.h index 4f9bf9c31d..77307ae74a 100644 --- a/include/xrpl/ledger/ReadView.h +++ b/include/xrpl/ledger/ReadView.h @@ -1,3 +1,17 @@ +/** @file + * Defines the foundational read-only ledger view interface. + * + * `ReadView` is the base of the entire ledger view hierarchy. Every concrete + * ledger representation — finalized `Ledger`, in-progress `OpenView`, apply-time + * `Sandbox`, or payment-path `PaymentSandbox` — exposes its state through this + * interface. Code that only reads ledger data can operate on any view type without + * knowing the concrete implementation. + * + * `DigestAwareReadView` extends `ReadView` with per-entry cryptographic digests, + * used by `CachedView` for efficient cache invalidation and by `makeRulesGivenLedger` + * to detect amendment changes between ledger closes. + */ + #pragma once #include @@ -21,21 +35,43 @@ namespace xrpl { //------------------------------------------------------------------------------ -/** A view into a ledger. - - This interface provides read access to state - and transaction items. There is no checkpointing - or calculation of metadata. -*/ +/** Pure abstract read-only interface to a ledger. + * + * Exposes two conceptually distinct maps: the **state map** (SLEs keyed by + * `uint256`) and the **transaction map** (committed transactions with metadata). + * Concrete implementations include `Ledger` (finalized), `OpenView` (in-progress), + * `Sandbox` (discardable apply-time copy), and `PaymentSandbox` (payment engine). + * + * @note Copy and move constructors explicitly re-initialize `sles` and `txs` + * with `*this`. Both members store a raw pointer to their owning view; a + * default memberwise copy would leave them pointing at the source object. + * Assignment operators are deleted for the same reason. + */ class ReadView { public: + /** Pair of transaction and its associated metadata object. + * + * The metadata `STObject` is empty for open ledgers, since metadata is + * only finalized at ledger close time. + */ using tx_type = std::pair, std::shared_ptr>; + /** Raw key type for state-map and transaction-map lookups. */ using key_type = uint256; + /** Shared ownership handle to a non-modifiable state entry. */ using mapped_type = std::shared_ptr; + /** STL-compatible forward range over the ledger state map. + * + * Iterates all SLEs present in this view. Backed by type-erased + * `ReadViewFwdIter` so the same interface works across SHAMap-backed, + * delta-list, and sandbox views. `upperBound` enables sub-range scans + * without a full traversal. + * + * @note Visiting every state entry can be expensive as the ledger grows. + */ struct SlesType : detail::ReadViewFwdRange> { explicit SlesType(ReadView const& view); @@ -43,13 +79,20 @@ public: begin() const; [[nodiscard]] Iterator end() const; + /** Returns an iterator to the first SLE whose key is strictly greater than @p key. */ [[nodiscard]] Iterator upperBound(key_type const& key) const; }; + /** STL-compatible forward range over the ledger transaction map. + * + * Iterates all `tx_type` pairs (transaction + metadata) present in + * this view. For open ledgers the metadata member of each pair is empty. + */ struct TxsType : detail::ReadViewFwdRange { explicit TxsType(ReadView const& view); + /** Returns `true` when the transaction map contains no entries. */ [[nodiscard]] bool empty() const; [[nodiscard]] Iterator @@ -65,92 +108,118 @@ public: ReadView& operator=(ReadView const& other) = delete; + /** Constructs the view and binds `sles` and `txs` to `*this`. */ ReadView() : sles(*this), txs(*this) { } + /** Copy-constructs the view, re-binding `sles` and `txs` to `*this`. + * + * @note The `sles` and `txs` members store a pointer to their owning + * view. They are explicitly re-initialized here to point at the new + * object, not at `other`. + */ ReadView(ReadView const& other) : sles(*this), txs(*this) { } + /** Move-constructs the view, re-binding `sles` and `txs` to `*this`. + * + * @note Same aliasing concern as the copy constructor; `sles` and `txs` + * are explicitly re-initialized to point at the new object. + */ ReadView(ReadView&& other) : sles(*this), txs(*this) { } - /** Returns information about the ledger. */ + /** Returns the immutable header fields for this ledger. + * + * All non-virtual convenience accessors (`seq()`, `parentCloseTime()`) + * delegate here, keeping the virtual dispatch surface minimal. + */ [[nodiscard]] virtual LedgerHeader const& header() const = 0; - /** Returns true if this reflects an open ledger. */ + /** Returns `true` if this view reflects an open (not yet closed) ledger. */ [[nodiscard]] virtual bool open() const = 0; - /** Returns the close time of the previous ledger. */ + /** Returns the close time of the previous (parent) ledger. */ [[nodiscard]] NetClock::time_point parentCloseTime() const { return header().parentCloseTime; } - /** Returns the sequence number of the base ledger. */ + /** Returns the sequence number of this ledger. */ [[nodiscard]] LedgerIndex seq() const { return header().seq; } - /** Returns the fees for the base ledger. */ + /** Returns the fee schedule in effect for this ledger. */ [[nodiscard]] virtual Fees const& fees() const = 0; - /** Returns the tx processing rules. */ + /** Returns the amendment rules active for this ledger. */ [[nodiscard]] virtual Rules const& rules() const = 0; - /** Determine if a state item exists. - - @note This can be more efficient than calling read. - - @return `true` if a SLE is associated with the - specified key. - */ + /** Returns `true` if a state entry matching the keylet is present. + * + * The `Keylet` bundles a raw `uint256` key with its `LedgerEntryType`, + * allowing implementations to reject type mismatches without deserializing + * the entry. This makes `exists` more efficient than calling `read` when + * only presence is needed. + * + * @param k The keylet (key + expected entry type) to probe. + * @return `true` if an SLE with the given key and type exists. + */ [[nodiscard]] virtual bool exists(Keylet const& k) const = 0; - /** Return the key of the next state item. - - This returns the key of the first state item - whose key is greater than the specified key. If - no such key is present, std::nullopt is returned. - - If `last` is engaged, returns std::nullopt when - the key returned would be outside the open - interval (key, last). - */ + /** Returns the smallest state-map key strictly greater than @p key. + * + * Enables ordered range scans of the SHAMap without deserializing entries. + * If @p last is set, the search is bounded to the open interval + * `(key, last)` — any candidate key outside that range causes + * `std::nullopt` to be returned instead. + * + * @param key The key to search above. + * @param last Optional exclusive upper bound for the result. + * @return The next key, or `std::nullopt` if none exists within bounds. + */ [[nodiscard]] virtual std::optional succ(key_type const& key, std::optional const& last = std::nullopt) const = 0; - /** Return the state item associated with a key. - - Effects: - If the key exists, gives the caller ownership - of the non-modifiable corresponding SLE. - - @note While the returned SLE is `const` from the - perspective of the caller, it can be changed - by other callers through raw operations. - - @return `nullptr` if the key is not present or - if the type does not match. - */ + /** Returns a read-only handle to the state entry identified by @p k. + * + * Gives the caller shared ownership of a non-modifiable SLE. The `const` + * qualifier reflects this caller's view; the underlying object may be + * mutated through `ApplyView` in another code path. + * + * @param k The keylet (key + expected entry type) to look up. + * @return Shared pointer to the SLE, or `nullptr` if the key is absent + * or the ledger entry type does not match the keylet. + */ [[nodiscard]] virtual std::shared_ptr read(Keylet const& k) const = 0; - // Accounts in a payment are not allowed to use assets acquired during that - // payment. The PaymentSandbox tracks the debits, credits, and owner count - // changes that accounts make during a payment. `balanceHookIOU` adjusts - // balances so newly acquired assets are not counted toward the balance. - // This is required to support PaymentSandbox. + /** Adjusts an IOU balance to exclude assets acquired during the current payment. + * + * The payment engine executes paths in reverse (destination-first), which + * means an account may be credited before it has redeemed the corresponding + * asset. Accounts must not spend assets acquired within the same payment. + * `PaymentSandbox` overrides this hook to subtract deferred credits recorded + * in its `DeferredCredits` table. The default implementation returns + * @p amount unchanged, making the hook zero-cost for non-payment views. + * + * @param account The account whose balance is being queried. + * @param issuer The IOU issuer. + * @param amount The raw IOU balance (must hold `Issue`). + * @return The effective spendable balance after deducting deferred credits. + */ [[nodiscard]] virtual STAmount balanceHookIOU(AccountID const& account, AccountID const& issuer, STAmount const& amount) const { @@ -159,71 +228,113 @@ public: return amount; } - // balanceHookMPT adjusts balances so newly acquired assets are not counted - // toward the balance. + /** Adjusts an MPT balance to exclude assets acquired during the current payment. + * + * Mirrors `balanceHookIOU` for MPT-denominated amounts. `PaymentSandbox` + * overrides this hook; the default implementation wraps @p amount in an + * `STAmount` and returns it unchanged. + * + * @param account The account whose balance is being queried. + * @param issue The MPT issuance. + * @param amount The raw MPT balance as a signed 64-bit integer. + * @return The effective spendable balance after deducting deferred credits. + */ [[nodiscard]] virtual STAmount balanceHookMPT(AccountID const& account, MPTIssue const& issue, std::int64_t amount) const { return STAmount{issue, amount}; } - // An offer owned by an issuer and selling MPT is limited by the issuer's - // funds available to issue, which are originally available funds less - // already self sold MPT amounts (MPT sell offer). This hook is used - // by issuerFundsToSelfIssue() function. + /** Adjusts the available issuance capacity for an issuer selling their own MPT. + * + * An issuer's sell-offer for their own MPT is limited by their remaining + * issuance capacity (i.e., `MaximumAmount - OutstandingAmount`), reduced + * by any MPT already committed to self-issued sell offers during this payment. + * `PaymentSandbox` overrides this hook to track that self-debit; the default + * returns @p amount unchanged. Used by `issuerFundsToSelfIssue()`. + * + * @param issue The MPT issuance. + * @param amount The raw available-issuance amount. + * @return The effective capacity after accounting for in-flight self-sold amounts. + */ [[nodiscard]] virtual STAmount balanceHookSelfIssueMPT(MPTIssue const& issue, std::int64_t amount) const { return STAmount{issue, amount}; } - // Accounts in a payment are not allowed to use assets acquired during that - // payment. The PaymentSandbox tracks the debits, credits, and owner count - // changes that accounts make during a payment. `ownerCountHook` adjusts the - // ownerCount so it returns the max value of the ownerCount so far. - // This is required to support PaymentSandbox. + /** Returns the effective owner count, adjusted for in-payment reserve changes. + * + * A payment could temporarily free reserves by consuming offers in intermediate + * steps, making it appear that an account has fewer owner-count obligations. + * `PaymentSandbox` overrides this hook to return the maximum owner count seen + * so far during the payment, preventing reserve-bypass exploits. The default + * implementation returns @p count unchanged. + * + * @param account The account being queried. + * @param count The current owner count from ledger state. + * @return The high-water-mark owner count for reserve purposes. + */ [[nodiscard]] virtual std::uint32_t ownerCountHook(AccountID const& account, std::uint32_t count) const { return count; } - // used by the implementation + /** Returns a heap-allocated iterator positioned at the start of the state map. + * + * Called by `SlesType::begin()`; not intended for direct use by callers. + */ [[nodiscard]] virtual std::unique_ptr slesBegin() const = 0; - // used by the implementation + /** Returns a heap-allocated sentinel iterator for the state map. + * + * Called by `SlesType::end()`; not intended for direct use by callers. + */ [[nodiscard]] virtual std::unique_ptr slesEnd() const = 0; - // used by the implementation + /** Returns a heap-allocated iterator to the first SLE whose key is strictly greater than @p key. + * + * Called by `SlesType::upperBound()`; not intended for direct use by callers. + */ [[nodiscard]] virtual std::unique_ptr slesUpperBound(key_type const& key) const = 0; - // used by the implementation + /** Returns a heap-allocated iterator positioned at the start of the transaction map. + * + * Called by `TxsType::begin()`; not intended for direct use by callers. + */ [[nodiscard]] virtual std::unique_ptr txsBegin() const = 0; - // used by the implementation + /** Returns a heap-allocated sentinel iterator for the transaction map. + * + * Called by `TxsType::end()`; not intended for direct use by callers. + */ [[nodiscard]] virtual std::unique_ptr txsEnd() const = 0; - /** Returns `true` if a tx exists in the tx map. - - A tx exists in the map if it is part of the - base ledger, or if it is a newly inserted tx. - */ + /** Returns `true` if a transaction with the given key exists in the tx map. + * + * A transaction is present if it is part of the base ledger or was + * inserted into this view's delta since the base. + * + * @param key The transaction hash to probe. + */ [[nodiscard]] virtual bool txExists(key_type const& key) const = 0; - /** Read a transaction from the tx map. - - If the view represents an open ledger, - the metadata object will be empty. - - @return A pair of nullptr if the - key is not found in the tx map. - */ + /** Returns the transaction and its metadata for the given key. + * + * For open ledgers the metadata `STObject` in the returned pair will be + * empty, since metadata is only finalized at close time. + * + * @param key The transaction hash to look up. + * @return A `tx_type` pair where both pointers are `nullptr` if the key + * is not found in the transaction map. + */ [[nodiscard]] virtual tx_type txRead(key_type const& key) const = 0; @@ -231,20 +342,29 @@ public: // Memberspaces // - /** Iterable range of ledger state items. - - @note Visiting each state entry in the ledger can - become quite expensive as the ledger grows. - */ + /** Iterable range over all state entries (SLEs) in this view. + * + * @note Full traversal can be expensive on a large ledger. Use + * `upperBound` or `succ` for targeted sub-range scans. + */ SlesType sles; - // The range of transactions + /** Iterable range over all transactions in this view. */ TxsType txs; }; //------------------------------------------------------------------------------ -/** ReadView that associates keys with digests. */ +/** Extension of `ReadView` that provides per-entry cryptographic digests. + * + * `Ledger` implements this interface cheaply by reading the hash directly + * from the SHAMap trie node without deserializing the leaf entry. Sandboxes + * and delta-views do not expose digests, which is why this capability is a + * separate subclass rather than part of `ReadView`. + * + * Used by `CachedView` for two-level cache invalidation and by + * `makeRulesGivenLedger` to detect amendments changes across ledger closes. + */ class DigestAwareReadView : public ReadView { public: @@ -253,19 +373,48 @@ public: DigestAwareReadView() = default; DigestAwareReadView(DigestAwareReadView const&) = default; - /** Return the digest associated with the key. - - @return std::nullopt if the item does not exist. - */ + /** Returns the cryptographic hash of the serialized state entry at @p key. + * + * Implementations may return this without fully deserializing the entry. + * + * @param key The raw state-map key to query. + * @return The entry's digest, or `std::nullopt` if no entry exists at that key. + */ [[nodiscard]] virtual std::optional digest(key_type const& key) const = 0; }; //------------------------------------------------------------------------------ +/** Constructs the active amendment `Rules` from a closed ledger, updating from existing rules. + * + * Reads the `sfAmendments` field from the ledger's amendments object and passes + * its digest to the `Rules` constructor so that `Rules` can detect unchanged + * amendments between successive ledger closes without re-parsing. Requires a + * `DigestAwareReadView` because the optimization depends on querying the entry + * hash directly. Falls back to a default `Rules` object if the amendments object + * is absent. + * + * @param ledger The closed ledger to read amendments from. + * @param current The current rules object; its internal preset set is forwarded + * to the new `Rules` instance. + * @return A `Rules` object reflecting the amendments active in @p ledger. + * @see makeRulesGivenLedger(DigestAwareReadView const&, std::unordered_set> const&) + */ Rules makeRulesGivenLedger(DigestAwareReadView const& ledger, Rules const& current); +/** Constructs the active amendment `Rules` from a closed ledger using an explicit preset set. + * + * Identical behavior to the `Rules const& current` overload but accepts + * the preset set directly. Used during initialization before a prior `Rules` + * object is available. + * + * @param ledger The closed ledger to read amendments from. + * @param presets The set of always-enabled amendment flags to seed the rules object. + * @return A `Rules` object reflecting the amendments active in @p ledger. + * @see makeRulesGivenLedger(DigestAwareReadView const&, Rules const&) + */ Rules makeRulesGivenLedger( DigestAwareReadView const& ledger, diff --git a/include/xrpl/ledger/Sandbox.h b/include/xrpl/ledger/Sandbox.h index dc80df5ba2..783c1011f1 100644 --- a/include/xrpl/ledger/Sandbox.h +++ b/include/xrpl/ledger/Sandbox.h @@ -5,12 +5,41 @@ namespace xrpl { -/** Discardable, editable view to a ledger. - - The sandbox inherits the flags of the base. - - @note Presented as ApplyView to clients. -*/ +/** Discardable staging layer for ledger mutations within a single transaction. + * + * `Sandbox` accumulates ledger changes in a private write buffer inherited + * from `detail::ApplyViewBase` without touching the underlying ledger. The + * caller decides at the end of the operation whether to commit — by calling + * `apply()` — or to discard — by letting the sandbox go out of scope. This + * eliminates the need for explicit rollback: on failure, destruction of the + * sandbox is sufficient. + * + * The typical pattern used by transactors: + * @code + * Sandbox sb(&ctx_.view()); + * auto const result = doWork(sb, ...); + * if (result == tesSUCCESS) + * sb.apply(ctx_.rawView()); + * @endcode + * + * `Sandbox` is the minimal concrete subclass of `ApplyViewBase`: it adds + * only constructors and `apply()`. It does not produce `TxMeta` (that is + * `ApplyViewImpl`'s responsibility) and does not track deferred credits (that + * is `PaymentSandbox`'s responsibility). Use `Sandbox` whenever a transactor + * or helper needs a safe, atomic scratchpad without those heavier features. + * + * The sandbox always inherits the `ApplyFlags` of its base view, so + * dry-run, no-check-sign, and similar execution-context properties propagate + * correctly through nested sandboxes without re-specification. + * + * Not copyable or move-assignable; move-constructible only. This enforces + * single ownership of the change buffer. + * + * @see detail::ApplyViewBase for the full `ApplyView`/`RawView` interface. + * @see ApplyViewImpl for the outermost commit path that also builds `TxMeta`. + * @see PaymentSandbox for the variant that prevents within-payment + * double-counting of credits. + */ class Sandbox : public detail::ApplyViewBase { public: @@ -23,14 +52,46 @@ public: Sandbox(Sandbox&&) = default; + /** Construct over any read-only ledger snapshot with explicit flags. + * + * @param base Non-owning pointer to the underlying ledger state; must + * outlive this sandbox. All reads that bypass the change buffer + * are forwarded here. + * @param flags Per-transaction policy flags (e.g. `tapDRY_RUN`, + * `tapNO_CHECK_SIGN`) governing this apply pass. + */ Sandbox(ReadView const* base, ApplyFlags flags) : ApplyViewBase(base, flags) { } + /** Construct over an existing `ApplyView`, inheriting its flags. + * + * Convenience form used when stacking a `Sandbox` on top of another + * mutable view (including another `Sandbox` or a `PaymentSandbox`). + * Flags are copied from the parent so that execution-context properties + * such as `tapDRY_RUN` propagate without the caller re-specifying them. + * + * @param base Non-owning pointer to the parent mutable view; must + * outlive this sandbox. + */ Sandbox(ApplyView const* base) : Sandbox(base, base->flags()) { } + /** Commit all buffered changes to a target `RawView`. + * + * Replays every insert, modify, and erase action accumulated in the + * internal change buffer against `to`, atomically promoting the tentative + * mutations into the target. After this call the buffer is reset; the + * sandbox must not be used again. + * + * If the caller decides the operation failed, simply do not call `apply()` + * — destroying the sandbox discards all buffered changes without touching + * the target view. + * + * @param to The target `RawView` to receive the committed mutations; + * typically `ctx_.rawView()` at the outermost transactor boundary. + */ void apply(RawView& to) { diff --git a/include/xrpl/ledger/View.h b/include/xrpl/ledger/View.h index 4958a89d8c..cfea538b91 100644 --- a/include/xrpl/ledger/View.h +++ b/include/xrpl/ledger/View.h @@ -19,6 +19,13 @@ namespace xrpl { +/** Controls whether `cleanupOnAccountDelete()` adjusts the directory iterator + * after a deletion. + * + * When `No`, the iterator position is decremented to compensate for the + * element shift caused by the deletion. When `Yes`, the entry was + * intentionally left in place by the deleter, so no adjustment is made. + */ enum class SkipEntry : bool { No = false, Yes }; //------------------------------------------------------------------------------ @@ -51,7 +58,21 @@ enum class SkipEntry : bool { No = false, Yes }; [[nodiscard]] bool hasExpired(ReadView const& view, std::optional const& exp); -// Note, depth parameter is used to limit the recursion depth +/** Determines whether a vault pseudo-account's MPT share token is indirectly + * frozen because the vault's underlying asset is frozen. + * + * Traverses: MPT issuance → issuer account root → vault object → vault asset, + * then delegates to `isAnyFrozen()`. Returns `false` immediately if the + * `featureSingleAssetVault` amendment is not enabled. + * + * @param view The ledger state to inspect. + * @param account The account whose holdings are being queried. + * @param mptShare The MPT share token issued by the vault pseudo-account. + * @param depth Recursion depth guard; returns `true` (conservatively frozen) + * if `depth >= kMAX_ASSET_CHECK_DEPTH`. + * @return `true` if the underlying asset is frozen for `account`; `false` + * otherwise or if the amendment is not enabled. + */ [[nodiscard]] bool isVaultPseudoAccountFrozen( ReadView const& view, @@ -59,6 +80,17 @@ isVaultPseudoAccountFrozen( MPTIssue const& mptShare, int depth); +/** Determines whether LP tokens for an AMM pool are frozen for an account. + * + * LP tokens are considered frozen if *either* constituent asset of the pool + * is frozen for `account`. + * + * @param view The ledger state to inspect. + * @param account The account whose holdings are being queried. + * @param asset The first asset of the AMM pool. + * @param asset2 The second asset of the AMM pool. + * @return `true` if either `asset` or `asset2` is frozen for `account`. + */ [[nodiscard]] bool isLPTokenFrozen( ReadView const& view, @@ -66,50 +98,94 @@ isLPTokenFrozen( Asset const& asset, Asset const& asset2); -// Return the list of enabled amendments +/** Returns the set of amendment hashes currently enabled on the ledger. + * + * Reads from the singleton `keylet::amendments()` SLE. If no amendments + * SLE exists or none are yet enabled, returns an empty set. + * + * @param view The ledger state to query. + * @return A `std::set` containing every enabled amendment hash. + */ [[nodiscard]] std::set getEnabledAmendments(ReadView const& view); -// Return a map of amendments that have achieved majority +/** Maps amendment hashes to the `NetClock::time_point` at which each first + * achieved validator supermajority. Used by the amendment governance process + * to enforce the two-week waiting period before activation. + */ using majorityAmendments_t = std::map; + +/** Returns amendments that have achieved validator supermajority but are not + * yet enabled. + * + * Reads the `sfMajorities` array from the singleton `keylet::amendments()` + * SLE and converts each entry's `sfCloseTime` to a `NetClock::time_point`. + * Returns an empty map if no SLE exists or no majority amendments are pending. + * + * @param view The ledger state to query. + * @return A `majorityAmendments_t` mapping each amendment hash to the time + * at which it first achieved supermajority. + */ [[nodiscard]] majorityAmendments_t getMajorityAmendments(ReadView const& view); -/** Return the hash of a ledger by sequence. - The hash is retrieved by looking up the "skip list" - in the passed ledger. As the skip list is limited - in size, if the requested ledger sequence number is - out of the range of ledgers represented in the skip - list, then std::nullopt is returned. - @return The hash of the ledger with the - given sequence number or std::nullopt. -*/ +/** Returns the hash of a past ledger by sequence number via the skip list. + * + * Implements a three-tier lookup: + * 1. **Trivial**: `seq == ledger.seq()` → returns the ledger's own hash; + * `seq == ledger.seq() - 1` → returns `parentHash` directly. + * 2. **Within 256**: Reads the rolling `keylet::skip()` object, which stores + * the hashes of the previous ≤ 256 ledgers, and indexes by offset. + * 3. **Aligned deep history**: For sequences that are multiples of 256, reads + * the permanent `LedgerHashes` page at `keylet::skip(seq)` and indexes into + * it. Non-aligned sequences beyond the 256-ledger rolling window are not + * reachable and return `std::nullopt`. + * + * @param ledger The view from whose skip list the search starts. + * @param seq The target ledger sequence number. + * @param journal Used to log warnings when the skip list is incomplete or the + * requested sequence is out of range. + * @return The hash of ledger `seq`, or `std::nullopt` if it cannot be + * determined from the available skip-list data. + */ [[nodiscard]] std::optional hashOfSeq(ReadView const& ledger, LedgerIndex seq, beast::Journal journal); -/** Find a ledger index from which we could easily get the requested ledger - - The index that we return should meet two requirements: - 1) It must be the index of a ledger that has the hash of the ledger - we are looking for. This means that its sequence must be equal to - greater than the sequence that we want but not more than 256 greater - since each ledger contains the hashes of the 256 previous ledgers. - - 2) Its hash must be easy for us to find. This means it must be 0 mod 256 - because every such ledger is permanently enshrined in a LedgerHashes - page which we can easily retrieve via the skip list. -*/ +/** Computes the nearest 256-aligned ledger sequence ≥ `requested`. + * + * Every ledger whose sequence is a multiple of 256 permanently stores a + * `LedgerHashes` page (`keylet::skip(seq)`) containing the hashes of + * the preceding 256 ledgers. That page is retrievable via the skip list, + * making it the ideal starting point for resolving an arbitrary past hash. + * The expression `(requested + 255) & (~255)` rounds up to the next 256 + * boundary in a single instruction. + * + * @param requested The target ledger sequence number. + * @return The smallest value ≥ `requested` that is divisible by 256. + */ inline LedgerIndex getCandidateLedger(LedgerIndex requested) { return (requested + 255) & (~255); } -/** Return false if the test ledger is provably incompatible - with the valid ledger, that is, they could not possibly - both be valid. Use the first form if you have both ledgers, - use the second form if you have not acquired the valid ledger yet -*/ +/** Returns `false` if `testLedger` is provably on a different chain than + * `validLedger`. + * + * Uses `hashOfSeq()` to walk the skip list of whichever ledger is later and + * confirms that the earlier ledger's hash appears in that list. A mismatch + * proves a fork. When the skip list is incomplete or the sequences are too + * far apart to compare, the function conservatively returns `true` (cannot + * prove incompatibility). Diagnostic lines are written to `s` on mismatch. + * + * Use this overload when both ledger objects are available. + * + * @param validLedger The authoritative ledger. + * @param testLedger The candidate ledger being verified. + * @param s Journal stream for diagnostic messages on mismatch. + * @param reason Short label prepended to log messages for context. + * @return `false` if a fork is proven; `true` otherwise. + */ [[nodiscard]] bool areCompatible( ReadView const& validLedger, @@ -117,6 +193,19 @@ areCompatible( beast::Journal::Stream& s, char const* reason); +/** Returns `false` if `testLedger` is provably on a different chain than the + * ledger identified by `(validHash, validIndex)`. + * + * Use this overload when the authoritative ledger object has not been fully + * loaded but its identity is known from consensus. + * + * @param validHash Hash of the authoritative ledger. + * @param validIndex Sequence number of the authoritative ledger. + * @param testLedger The candidate ledger being verified. + * @param s Journal stream for diagnostic messages on mismatch. + * @param reason Short label prepended to log messages for context. + * @return `false` if a fork is proven; `true` otherwise. + */ [[nodiscard]] bool areCompatible( uint256 const& validHash, @@ -131,6 +220,19 @@ areCompatible( // //------------------------------------------------------------------------------ +/** Inserts an SLE into an account's owner directory and records the page. + * + * Calls `view.dirInsert()` to append `object` to `owner`'s owner directory, + * then writes the assigned page number back into `object`'s `node` field. + * + * @param view The mutable ledger view. + * @param owner The account whose owner directory receives the entry. + * @param object The SLE being linked; updated in-place with the page number. + * @param node The field on `object` that receives the directory page number; + * defaults to `sfOwnerNode`. + * @return `tecDIR_FULL` if the owner directory has no room; `tesSUCCESS` + * otherwise. + */ [[nodiscard]] TER dirLink( ApplyView& view, @@ -138,19 +240,30 @@ dirLink( std::shared_ptr& object, SF_UINT64 const& node = sfOwnerNode); -/** Checks that can withdraw funds from an object to itself or a destination. +/** Checks whether funds can be withdrawn from `from` to `to` given a + * pre-fetched destination SLE. * - * The receiver may be either the submitting account (sfAccount) or a different - * destination account (sfDestination). + * This is the innermost overload; use it when the caller already holds `toSle` + * to avoid a redundant ledger read. Rules enforced in order: + * - `toSle` must be non-null (destination account must exist). + * - If `lsfRequireDestTag` is set, `hasDestinationTag` must be `true` even + * for self-sends. + * - If `from == to`, succeed immediately. + * - If `lsfDepositAuth` is set, `from` must have a pre-authorized + * `DepositPreauth` entry under `to`. + * - For IOU amounts, the withdrawal must not push `to` past its trust-line + * credit limit. MPT transfers skip this check because they move existing + * supply rather than creating new tokens. * - * - Checks that the receiver account exists. - * - If the receiver requires a destination tag, check that one exists, even - * if withdrawing to self. - * - If withdrawing to self, succeed. - * - If not, checks if the receiver requires deposit authorization, and if - * the sender has it. - * - Checks that the receiver will not exceed the limit (IOU trustline limit - * or MPT MaximumAmount). + * @param view Ledger state to query. + * @param from Source account (e.g., vault or broker pseudo-account). + * @param to Destination account. + * @param toSle Pre-fetched SLE for `to`; may be null. + * @param amount Asset and quantity being transferred. + * @param hasDestinationTag Whether the transaction includes `sfDestinationTag`. + * @return `tesSUCCESS`, or a `tec` code: `tecNO_DST` (account absent), + * `tecDST_TAG_NEEDED` (tag missing), `tecNO_PERMISSION` (deposit auth + * denied), or `tecNO_LINE` (IOU limit exceeded). */ [[nodiscard]] TER canWithdraw( @@ -161,19 +274,17 @@ canWithdraw( STAmount const& amount, bool hasDestinationTag); -/** Checks that can withdraw funds from an object to itself or a destination. +/** Checks whether funds can be withdrawn from `from` to `to`. * - * The receiver may be either the submitting account (sfAccount) or a different - * destination account (sfDestination). + * Looks up the destination account SLE and delegates to the six-argument + * overload. See that overload for the full rule set. * - * - Checks that the receiver account exists. - * - If the receiver requires a destination tag, check that one exists, even - * if withdrawing to self. - * - If withdrawing to self, succeed. - * - If not, checks if the receiver requires deposit authorization, and if - * the sender has it. - * - Checks that the receiver will not exceed the limit (IOU trustline limit - * or MPT MaximumAmount). + * @param view Ledger state to query. + * @param from Source account. + * @param to Destination account. + * @param amount Asset and quantity being transferred. + * @param hasDestinationTag Whether the transaction includes `sfDestinationTag`. + * @return `tesSUCCESS` or a `tec` code; see the six-argument overload. */ [[nodiscard]] TER canWithdraw( @@ -183,23 +294,45 @@ canWithdraw( STAmount const& amount, bool hasDestinationTag); -/** Checks that can withdraw funds from an object to itself or a destination. +/** Checks whether the withdrawal described by `tx` is permitted. * - * The receiver may be either the submitting account (sfAccount) or a different - * destination account (sfDestination). + * Extracts `sfAccount`, `sfDestination` (defaults to `sfAccount` when absent), + * `sfAmount`, and the presence of `sfDestinationTag` from the transaction, then + * delegates to the five-argument overload. Intended for use in preclaim. * - * - Checks that the receiver account exists. - * - If the receiver requires a destination tag, check that one exists, even - * if withdrawing to self. - * - If withdrawing to self, succeed. - * - If not, checks if the receiver requires deposit authorization, and if - * the sender has it. - * - Checks that the receiver will not exceed the limit (IOU trustline limit - * or MPT MaximumAmount). + * @param view Ledger state to query. + * @param tx The withdrawal transaction (e.g., `VaultWithdraw` or + * `LoanBrokerCoverWithdraw`). + * @return `tesSUCCESS` or a `tec` code; see the six-argument overload. */ [[nodiscard]] TER canWithdraw(ReadView const& view, STTx const& tx); +/** Executes the physical asset transfer from a pseudo-account to a destination. + * + * When `dstAcct == senderAcct` (self-withdrawal), calls `addEmptyHolding()` + * to lazily create a trust line or MPToken record if one does not already + * exist (`tecDUPLICATE` is silently tolerated). For third-party + * destinations, calls `verifyDepositPreauth()` to enforce deposit + * authorisation and prune any expired credential objects as a side-effect. + * + * Before transferring, asserts via `accountHolds()` that `sourceAcct` holds + * at least `amount`; a shortfall surfaces as `tefINTERNAL` rather than an + * overdraft. On success, calls `accountSend()` with `WaiveTransferFee::Yes`. + * + * @param view The mutable ledger view. + * @param tx The originating transaction (used by `verifyDepositPreauth`). + * @param senderAcct The transaction submitter / withdrawal beneficiary. + * @param dstAcct The account that will receive the funds. + * @param sourceAcct The pseudo-account (vault, loan broker) holding the funds. + * @param priorBalance The XRP balance of `senderAcct` before the transaction, + * used for reserve calculation when creating an empty holding. + * @param amount The asset and quantity to transfer. + * @param j Journal for diagnostic logging. + * @return `tesSUCCESS` on success; `tefINTERNAL` if the source has + * insufficient balance; any TER propagated from `verifyDepositPreauth` or + * `accountSend` otherwise. + */ [[nodiscard]] TER doWithdraw( ApplyView& view, @@ -211,18 +344,41 @@ doWithdraw( STAmount const& amount, beast::Journal j); -/** Deleter function prototype. Returns the status of the entry deletion - * (if should not be skipped) and if the entry should be skipped. The status - * is always tesSUCCESS if the entry should be skipped. +/** Callback invoked by `cleanupOnAccountDelete()` for each owner-directory entry. + * + * Returns a pair: + * - `TER` — `tesSUCCESS` if the entry was handled or intentionally skipped; + * any other code aborts the cleanup loop immediately. + * - `SkipEntry` — `Yes` if the entry was left in place (iterator must not be + * decremented); `No` if the entry was removed (iterator must be decremented + * to compensate for the index shift). + * + * The `TER` value is always `tesSUCCESS` when `SkipEntry` is `Yes`. */ using EntryDeleter = std::function< std::pair(LedgerEntryType, uint256 const&, std::shared_ptr&)>; -/** Cleanup owner directory entries on account delete. - * Used for a regular and AMM accounts deletion. The caller - * has to provide the deleter function, which handles details of - * specific account-owned object deletion. - * @return tecINCOMPLETE indicates maxNodesToDelete - * are deleted and there remains more nodes to delete. + +/** Iterates an account's owner directory and removes entries via `deleter`. + * + * Used by `DeleteAccount` and AMM account deletion. Traversal uses the + * `dirFirst`/`dirNext` exposed-cursor pattern; after each successful removal + * the cursor is decremented by one to compensate for the index shift that + * occurs when an element is erased mid-iteration. When the deleter leaves an + * entry in place (`SkipEntry::Yes`), the cursor is not adjusted. + * + * When `maxNodesToDelete` is supplied and the limit is reached before the + * directory is empty, `tecINCOMPLETE` is returned, signaling the caller that + * the account-delete transaction must be retried in a future ledger. + * + * @param view Mutable ledger view. + * @param ownerDirKeylet Keylet of the account's owner directory root. + * @param deleter Callback invoked once per directory entry. + * @param j Journal for diagnostic logging. + * @param maxNodesToDelete Optional cap on entries processed per call. + * When absent, all entries are consumed in a single invocation. + * @return `tesSUCCESS` when the directory is fully processed; + * `tecINCOMPLETE` if `maxNodesToDelete` is exhausted with entries + * remaining; `tefBAD_LEDGER` if a ledger invariant is violated. */ [[nodiscard]] TER cleanupOnAccountDelete( diff --git a/include/xrpl/ledger/detail/ApplyStateTable.h b/include/xrpl/ledger/detail/ApplyStateTable.h index 7b18f742b4..c49128df7d 100644 --- a/include/xrpl/ledger/detail/ApplyStateTable.h +++ b/include/xrpl/ledger/detail/ApplyStateTable.h @@ -1,3 +1,9 @@ +/** @file + * Declares `ApplyStateTable`, the per-transaction write-staging buffer used + * by all `ApplyView`/`ApplyViewImpl` instances. This is an implementation + * detail of `ApplyViewBase` and is not intended for direct use by transactors. + */ + #pragma once #include @@ -12,18 +18,36 @@ namespace xrpl::detail { -// Helper class that buffers modifications +/** Write-staging buffer for a single transaction's ledger mutations. + * + * Every SLE touched by a transaction is recorded here — keyed by its + * `uint256` ledger key — along with an `Action` tag that tracks whether + * the entry was merely read (`Cache`), newly created (`Insert`), mutated + * (`Modify`), or scheduled for removal (`Erase`). On success the buffer + * is flushed atomically to the underlying view; on failure the table is + * simply discarded. + * + * The class is the core member of `ApplyViewBase` and backs all + * `ApplyView`/`ApplyViewImpl` instances that transactors receive. + * + * @note Not copyable. Move-constructible only to support placement inside + * `ApplyViewBase` during construction. + * @note `erase()` and `update()` enforce pointer-identity: the caller + * must pass the exact `shared_ptr` returned by `peek()` on this same + * table instance. Crossing views is a `LogicError`. + */ class ApplyStateTable { public: using key_type = ReadView::key_type; private: + /** Lifecycle state of a buffered ledger entry. */ enum class Action { - Cache, - Erase, - Insert, - Modify, + Cache, /**< Read from base; no write intent yet. */ + Erase, /**< Scheduled for deletion from the base view. */ + Insert, /**< New object not yet in the base view. */ + Modify, /**< Existing object with pending mutations. */ }; using items_t = std::map>>; @@ -41,9 +65,48 @@ public: ApplyStateTable& operator=(ApplyStateTable const&) = delete; + /** Flush all pending mutations to a raw view without generating metadata. + * + * Maps each buffered action to a raw write on `to`: `Cache` entries + * are skipped; `Erase` → `rawErase`; `Insert` → `rawInsert`; + * `Modify` → `rawReplace`. Also forwards the accumulated + * `dropsDestroyed_` to `to.rawDestroyXRP()`. + * + * Used when committing a sandbox or nested view back to its parent. + * + * @param to The target raw view to receive the mutations. + */ void apply(RawView& to) const; + /** Flush mutations to an open view, generating `TxMeta` for closed ledgers. + * + * For closed ledgers (`!to.open()`) or dry-run mode (`isDryRun`), + * builds full `TxMeta` — classifying every pending item as + * `sfCreatedNode`, `sfModifiedNode`, or `sfDeletedNode` — and + * populates `sfPreviousFields`/`sfFinalFields`/`sfNewFields` using + * `SField` metadata flags. Threads `sfPreviousTxnID`/ + * `sfPreviousTxnLgrSeq` onto affected account roots and trust-line + * endpoints. + * + * In dry-run mode the metadata is produced but state changes and the + * raw tx insert are suppressed — supporting fee simulation without + * side effects. + * + * A `sfModifiedNode` whose buffered content is byte-for-byte equal to + * the original is silently omitted from the metadata. + * + * @param to The open view to commit into. + * @param tx The transaction being applied. + * @param ter The transaction result code; recorded in the metadata. + * @param deliver Optional delivered amount annotation for the metadata. + * @param parentBatchId Optional batch parent ID for the metadata. + * @param isDryRun If true, produce metadata but suppress state mutations. + * @param j Journal for diagnostic logging. + * @return The generated `TxMeta` when `!to.open() || isDryRun`; + * `std::nullopt` when the view is open and `isDryRun` is false + * (live open-ledger apply, no metadata needed). + */ std::optional apply( OpenView& to, @@ -54,21 +117,88 @@ public: bool isDryRun, beast::Journal j); + /** Test whether a ledger object exists, accounting for pending changes. + * + * Returns `false` for objects pending `Erase`; returns `true` for + * objects buffered as `Cache`, `Insert`, or `Modify`; falls back to + * `base.exists(k)` for keys not yet in the buffer. + * + * @param base The underlying read view (base ledger state). + * @param k The keylet identifying the object to test. + * @return `true` if the object will exist after the pending changes. + */ [[nodiscard]] bool exists(ReadView const& base, Keylet const& k) const; + /** Find the smallest key strictly greater than `key` that will exist + * after applying pending changes, up to but not including `last`. + * + * Merges two sorted key spaces: the base ledger (skipping keys + * pending deletion) and the local `items_` map (skipping erased + * entries). Returns whichever candidate is smaller. + * + * @param base The underlying read view supplying the base key space. + * @param key The starting key (exclusive lower bound). + * @param last Optional exclusive upper bound; if the result reaches + * or exceeds `last`, `std::nullopt` is returned. + * @return The next live key, or `std::nullopt` if none exists in + * range. + */ [[nodiscard]] std::optional succ(ReadView const& base, key_type const& key, std::optional const& last) const; + /** Read a ledger object as an immutable snapshot, accounting for + * pending changes. + * + * Returns `nullptr` for objects pending `Erase` or whose keylet + * check fails; returns the buffered SLE for `Cache`, `Insert`, and + * `Modify` entries; falls back to `base.read(k)` for unknown keys. + * + * @param base The underlying read view. + * @param k The keylet identifying the object. + * @return A `const`-qualified `shared_ptr` to the SLE, or `nullptr` + * if the object does not exist or the keylet check fails. + */ [[nodiscard]] std::shared_ptr read(ReadView const& base, Keylet const& k) const; + /** Obtain a mutable handle to a ledger object, loading it on first + * access. + * + * If the key is not yet in the buffer, reads from `base` and stores + * a private copy under `Action::Cache`. Subsequent calls return the + * same `shared_ptr`. Returns `nullptr` for erased objects or when the + * object does not exist in `base`. + * + * The returned pointer is the exact instance that must be passed to + * `update()` or `erase()` — pointer identity is enforced. + * + * @param base The underlying read view. + * @param k The keylet identifying the object. + * @return A mutable `shared_ptr` to the buffered SLE, or `nullptr`. + */ std::shared_ptr peek(ReadView const& base, Keylet const& k); + /** Count pending mutations (Erase, Insert, Modify), excluding cache-only reads. + * + * @return The number of entries with a write-intent action. + */ [[nodiscard]] std::size_t size() const; + /** Invoke a callback for every pending write-intent entry. + * + * Calls `func` once for each `Erase`, `Insert`, or `Modify` entry in + * the buffer. `Cache`-only entries are skipped. The `before` snapshot + * is read from `base` on each call; `after` is the buffered SLE. + * + * @param base The underlying read view used to fetch the pre-change + * snapshots for `Erase` and `Modify` entries. + * @param func Callback invoked as + * `func(key, isDelete, before, after)`. `before` is `nullptr` + * for `Insert`; `after` is the pending SLE in all cases. + */ void visit( ReadView const& base, @@ -78,25 +208,95 @@ public: std::shared_ptr const& before, std::shared_ptr const& after)> const& func) const; + /** Mark a buffered object for deletion. + * + * Transitions the action from `Cache` or `Modify` to `Erase`. If the + * object was previously `Insert`ed within this same transaction, the + * entry is removed entirely (net-zero effect on the base). Calling on + * an unknown key or a different `shared_ptr` than the one returned by + * `peek()` is a `LogicError`. + * + * @param base The underlying read view (used for key lookup context). + * @param sle The exact `shared_ptr` previously obtained from `peek()`. + * @throws std::logic_error If the key is not in the buffer, the + * pointer does not match, or the entry is already erased. + */ void erase(ReadView const& base, std::shared_ptr const& sle); + /** Mark an object for deletion without enforcing pointer identity. + * + * Behaves like `erase()` but accepts any SLE with the matching key — + * the caller-provided pointer replaces whatever is stored. Used by + * `ApplyViewBase` for raw-level operations that bypass the ownership + * protocol enforced by `erase()`. + * + * @param base The underlying read view (used for key lookup context). + * @param sle An SLE whose key identifies the object to erase. + * @throws std::logic_error If the object is already pending erasure. + */ void rawErase(ReadView const& base, std::shared_ptr const& sle); + /** Stage a new ledger object for insertion. + * + * Records the SLE under `Action::Insert`. If the key was previously + * erased within this same transaction, the action is collapsed to + * `Action::Modify` (insert-after-erase = replace). Attempting to + * insert over an existing `Cache`, `Insert`, or `Modify` entry is + * a `LogicError`. + * + * @param base The underlying read view (used for key lookup context). + * @param sle The new SLE to insert. + * @throws std::logic_error If the key already exists with a + * non-erase action. + */ void insert(ReadView const& base, std::shared_ptr const& sle); + /** Promote a cached or new SLE to a definitive write. + * + * Requires the exact `shared_ptr` returned by `peek()`. Transitions + * `Cache` → `Modify`; `Insert` and `Modify` are left unchanged + * (already write-intent). Calling on an erased or unknown entry is a + * `LogicError`. + * + * @param base The underlying read view (used for key lookup context). + * @param sle The exact `shared_ptr` previously obtained from `peek()`. + * @throws std::logic_error If the key is missing, the pointer does not + * match, or the entry is already erased. + */ void update(ReadView const& base, std::shared_ptr const& sle); + /** Unconditionally overwrite the buffered SLE for a given key. + * + * Records the SLE under `Action::Modify`, replacing any existing + * `Cache` or `Insert` entry with the supplied pointer. Calling on an + * erased entry is a `LogicError`. Unlike `update()`, does not enforce + * pointer identity — the caller supplies a fresh SLE. + * + * @param base The underlying read view (used for key lookup context). + * @param sle The SLE to store. + * @throws std::logic_error If the key is currently pending erasure. + */ void replace(ReadView const& base, std::shared_ptr const& sle); + /** Record XRP drops destroyed by fees within this transaction's scope. + * + * Accumulates into `dropsDestroyed_`, which is forwarded to + * `RawView::rawDestroyXRP()` on `apply()`. + * + * @param fee The amount of XRP to permanently remove from circulation. + */ void destroyXRP(XRPAmount const& fee); - // For debugging + /** Return the total XRP drops marked for destruction so far. + * + * @return Reference to the accumulated destroyed-drops counter. + */ [[nodiscard]] XRPAmount const& dropsDestroyed() const { @@ -104,17 +304,74 @@ public: } private: + /** Scratch map used during threading to track SLEs modified solely by + * metadata updates (i.e., objects whose only change is the addition of + * `sfPreviousTxnID`/`sfPreviousTxnLgrSeq` fields). These are kept + * separate from `items_` to avoid promoting cache entries to + * `Action::Modify` for transactional purposes. + */ using Mods = hash_map>; + /** Update an SLE's thread fields and record the previous tx link in metadata. + * + * Calls `sle->thread(txID, lgrSeq, ...)` to update `sfPreviousTxnID` + * and `sfPreviousTxnLgrSeq` in place. If there was a preceding + * transaction, adds those old fields to the affected node in `meta` + * so the chain of transactions is visible in on-ledger metadata. + * + * @param meta The `TxMeta` object being built for the current transaction. + * @param sle The account-root SLE to thread. + */ static void threadItem(TxMeta& meta, std::shared_ptr const& to); + /** Retrieve an SLE for threading modification, using `mods` as a cache. + * + * Checks `mods` first, then `items_` (returning non-cache entries + * directly), then falls back to `base`. Objects found in `items_` as + * `Action::Cache` are copied into `mods` so that threading-only + * mutations do not promote them to `Action::Modify` in the primary + * table. Returns `nullptr` when threading to a deleted or nonexistent + * account (e.g., an expired Escrow destination), which is legal. + * + * @param base The underlying read view. + * @param key The ledger key of the SLE to retrieve. + * @param mods Scratch map of threading-only modifications. + * @param j Journal for warnings about missing or deleted targets. + * @return A mutable SLE, or `nullptr` if the account does not exist. + */ std::shared_ptr getForMod(ReadView const& base, key_type const& key, Mods& mods, beast::Journal j); + /** Thread the current transaction to a specific account's root SLE. + * + * Looks up the account root via `getForMod()` and calls `threadItem()` + * on it. Logs a warning and returns without error if the account does + * not exist (e.g., destination of a deleted Escrow or PayChannel). + * + * @param base The underlying read view. + * @param meta The `TxMeta` object being built. + * @param to The account whose root SLE should be threaded. + * @param mods Scratch map of threading-only modifications. + * @param j Journal for warnings about missing targets. + */ void threadTx(ReadView const& base, TxMeta& meta, AccountID const& to, Mods& mods, beast::Journal j); + /** Thread the current transaction to all owner accounts of a ledger entry. + * + * Dispatches by `LedgerEntryType`: + * - `ltACCOUNT_ROOT`: no-op (threading to self is handled by the caller). + * - `ltRIPPLE_STATE`: threads to both the low-limit and high-limit account. + * - All others: threads to `sfAccount` if present, and to `sfDestination` + * if present. + * + * @param base The underlying read view. + * @param meta The `TxMeta` object being built. + * @param sle The ledger entry whose owner accounts should be threaded. + * @param mods Scratch map of threading-only modifications. + * @param j Journal for warnings about missing targets. + */ void threadOwners( ReadView const& base, diff --git a/include/xrpl/ledger/detail/ApplyViewBase.h b/include/xrpl/ledger/detail/ApplyViewBase.h index 558c9e5d4d..1b8520bedf 100644 --- a/include/xrpl/ledger/detail/ApplyViewBase.h +++ b/include/xrpl/ledger/detail/ApplyViewBase.h @@ -1,3 +1,14 @@ +/** @file + * Declares `ApplyViewBase`, the abstract concrete base class shared by all + * buffered mutable ledger views used during transaction application. + * + * `ApplyViewBase` lives in `xrpl::detail` to signal that it is internal + * infrastructure; transaction processing code works with `ApplyView` or + * `ApplyViewImpl` references. The three concrete subclasses — + * `ApplyViewImpl`, `Sandbox`, and `PaymentSandbox` — are the only types + * that need to reach into this layer directly. + */ + #pragma once #include @@ -7,6 +18,24 @@ namespace xrpl::detail { +/** Concrete base for buffered mutable ledger views. + * + * Implements the full `ApplyView` and `RawView` interfaces on top of two + * members: a read-only pointer to the base ledger snapshot (`base_`) and + * an `ApplyStateTable` change buffer (`items_`). Queries that need + * awareness of pending mutations (e.g. `exists`, `read`, `peek`) merge + * `items_` with `base_`; purely structural queries (`header`, `fees`, + * `rules`) and SLE iterators bypass `items_` and forward directly to + * `base_`. + * + * Not copyable; move-constructible only. Subclasses (`ApplyViewImpl`, + * `Sandbox`) supply lifecycle logic such as `apply()`. + * + * @note The `erase()` and `update()` methods enforce pointer identity: the + * caller must pass the exact `shared_ptr` returned by `peek()` on + * **this** view instance. Passing an SLE obtained from a different view + * results in a `LogicError`. + */ class ApplyViewBase : public ApplyView, public RawView { public: @@ -19,85 +48,254 @@ public: ApplyViewBase(ApplyViewBase&&) = default; + /** Construct over an existing read-only ledger snapshot. + * + * @param base Non-owning pointer to the base ledger state; must + * outlive this view. All reads that bypass the change buffer + * are forwarded here. + * @param flags Per-transaction policy flags (retry mode, dry-run, + * unlimited, etc.) that are carried through the apply pass and + * exposed via `flags()`. + */ ApplyViewBase(ReadView const* base, ApplyFlags flags); // ReadView + + /** @return `true` if the underlying view represents an open ledger. */ [[nodiscard]] bool open() const override; + /** @return The ledger header from the base snapshot. */ [[nodiscard]] LedgerHeader const& header() const override; + /** @return The fee schedule from the base snapshot. */ [[nodiscard]] Fees const& fees() const override; + /** @return The amendment rules from the base snapshot. */ [[nodiscard]] Rules const& rules() const override; + /** Test whether a ledger object exists, accounting for pending changes. + * + * Returns `false` for objects pending erasure, `true` for objects + * buffered as inserted or modified, and delegates to `base_` for + * keys not yet in the change buffer. + * + * @param k Keylet identifying the object. + * @return `true` if the object will exist after the pending changes. + */ [[nodiscard]] bool exists(Keylet const& k) const override; + /** Find the next live key after `key`, accounting for pending changes. + * + * Merges the base key space (skipping keys pending deletion) with the + * local change buffer (skipping erased entries) and returns the smaller + * candidate key that is less than `last`. + * + * @param key Exclusive lower bound. + * @param last Optional exclusive upper bound. + * @return The next live key, or `std::nullopt` if none exists in range. + */ [[nodiscard]] std::optional succ(key_type const& key, std::optional const& last = std::nullopt) const override; + /** Read a ledger object as an immutable snapshot, accounting for pending + * changes. + * + * Returns `nullptr` for objects pending erasure or when the keylet check + * fails; returns the buffered SLE for inserted/modified entries; falls + * back to `base_` for unknown keys. + * + * @param k Keylet identifying the object. + * @return A `const`-qualified handle to the SLE, or `nullptr`. + */ [[nodiscard]] std::shared_ptr read(Keylet const& k) const override; + /** @name SLE iterators (base snapshot only) + * + * These iterators forward directly to `base_` and do **not** reflect + * pending insertions or deletions in the change buffer. This is + * intentional: the apply phase never needs to iterate its own buffered + * writes, and bypassing the buffer keeps SLE traversal consistent with + * the base ledger snapshot. + */ + /** @{ */ [[nodiscard]] std::unique_ptr slesBegin() const override; [[nodiscard]] std::unique_ptr slesEnd() const override; + /** Return an iterator to the first SLE whose key is not less than `key`, + * drawn from the base snapshot only. + * + * @param key The lower-bound key for the search. + * @return An iterator into the base SLE map at or after `key`. + */ [[nodiscard]] std::unique_ptr slesUpperBound(uint256 const& key) const override; + /** @} */ + /** @name Transaction-map accessors (forwarded to base snapshot) */ + /** @{ */ [[nodiscard]] std::unique_ptr txsBegin() const override; [[nodiscard]] std::unique_ptr txsEnd() const override; + /** Test whether a transaction exists in the base snapshot's tx-map. + * + * @param key The transaction ID to look up. + * @return `true` if the transaction is present in the base ledger's + * transaction map. + */ [[nodiscard]] bool txExists(key_type const& key) const override; + /** Read a transaction and its metadata from the base snapshot's tx-map. + * + * @param key The transaction ID to retrieve. + * @return A pair of `(STTx, STObject metadata)` for the transaction, + * or `{nullptr, nullptr}` if not found. + */ [[nodiscard]] tx_type txRead(key_type const& key) const override; + /** @} */ // ApplyView + /** Return the flags governing this transaction apply pass. + * + * @return The `ApplyFlags` bitmask set at construction. + */ [[nodiscard]] ApplyFlags flags() const override; + /** Check out a ledger entry for in-place mutation. + * + * Loads the entry into the change buffer on first access (tagged + * `Cache`). Returns the same `shared_ptr` on subsequent calls. + * The returned pointer must later be passed to `update()` or `erase()` + * on **this** view instance to record the intended change. + * + * @param k Keylet identifying the entry. + * @return A mutable handle to the buffered SLE, or `nullptr` if the + * entry does not exist (including if it is pending erasure). + */ std::shared_ptr peek(Keylet const& k) override; + /** Stage a deletion for a checked-out entry. + * + * Transitions the buffer entry from `Cache` or `Modify` to `Erase`. + * If the entry was inserted within this same transaction, it is removed + * entirely (net-zero effect on the base). + * + * @param sle The exact `shared_ptr` previously returned by `peek()` + * on this view instance. + * @throws std::logic_error If the pointer does not match the buffered + * entry or the entry is already erased. + */ void erase(std::shared_ptr const& sle) override; + /** Stage a new ledger entry for insertion. + * + * Records the SLE under `Action::Insert`. If the key was previously + * erased within this transaction the action is collapsed to `Modify`. + * + * @param sle The new entry; its key must not already exist in the view. + * @throws std::logic_error If the key already exists with a non-erase + * action in the buffer. + */ void insert(std::shared_ptr const& sle) override; + /** Promote a checked-out entry to a definitive write. + * + * Transitions the buffer action from `Cache` to `Modify`; `Insert` and + * `Modify` entries are left unchanged (already write-intent). + * + * @param sle The exact `shared_ptr` previously returned by `peek()` + * on this view instance. + * @throws std::logic_error If the pointer does not match, the entry is + * erased, or the key is unknown. + */ void update(std::shared_ptr const& sle) override; // RawView + /** Erase a ledger entry without enforcing pointer-identity ownership. + * + * Bypasses the `peek()`-pointer ownership check enforced by `erase()`. + * Used by `RawView` callers (e.g. `Sandbox::apply`) that flush changes + * from another view's table and cannot satisfy the same-instance + * invariant. + * + * @param sle An SLE whose key identifies the object to erase. + * @throws std::logic_error If the object is already pending erasure. + */ void rawErase(std::shared_ptr const& sle) override; + /** Insert a ledger entry via the same validated path as `insert()`. + * + * Despite being a raw-tier operation, this method calls the same + * `items_.insert()` that the high-level `insert()` uses; the + * distinction is that callers from the `RawView` flush path are not + * required to have obtained the SLE from `peek()`. + * + * @param sle The new entry to stage for insertion. + * @throws std::logic_error If the key already exists with a non-erase + * action. + */ void rawInsert(std::shared_ptr const& sle) override; + /** Unconditionally overwrite the buffered SLE for an existing key. + * + * Records the SLE under `Action::Modify`, replacing any `Cache` or + * `Insert` entry. Unlike `update()`, does not enforce pointer identity. + * + * @param sle The SLE to store; its key must exist in this view. + * @throws std::logic_error If the key is currently pending erasure. + */ void rawReplace(std::shared_ptr const& sle) override; + /** Record XRP drops destroyed by fees within this transaction's scope. + * + * Accumulates into the change buffer and is forwarded to the parent + * view's `rawDestroyXRP()` when the buffer is committed. + * + * @param feeDrops The amount of XRP to permanently remove from + * circulation. + */ void rawDestroyXRP(XRPAmount const& feeDrops) override; protected: + /** Per-transaction policy flags set at construction; exposed via `flags()`. */ ApplyFlags flags_; + + /** Non-owning pointer to the base ledger snapshot. + * + * All reads that do not need awareness of pending changes are forwarded + * here. The pointed-to view must outlive this object. + */ ReadView const* base_; + + /** Change buffer accumulating per-transaction ledger mutations. + * + * Maps each touched `uint256` key to an `(Action, SLE)` pair. Flushed + * to the parent view atomically on `apply()`; discarded on destruction. + */ detail::ApplyStateTable items_; }; diff --git a/include/xrpl/ledger/detail/RawStateTable.h b/include/xrpl/ledger/detail/RawStateTable.h index 169a7c505e..c87e35e9e4 100644 --- a/include/xrpl/ledger/detail/RawStateTable.h +++ b/include/xrpl/ledger/detail/RawStateTable.h @@ -11,27 +11,73 @@ namespace xrpl::detail { -// Helper class that buffers raw modifications +/** In-memory write buffer that accumulates SLE mutations before flushing them + * to a backing `RawView`. + * + * Every mutable ledger view (`OpenView`, and indirectly `ApplyStateTable`) + * embeds a `RawStateTable` as its delta accumulator. The three mutation + * methods — `erase`, `insert`, and `replace` — apply a state-machine + * collapse so the map stays minimal: insert-then-erase cancels out entirely; + * erase-then-insert upgrades to replace; and illegal sequences (double-erase, + * double-insert) throw `std::logic_error`. `read`, `exists`, and `succ` + * overlay the pending delta transparently onto the supplied base `ReadView`, + * so callers always see a coherent merged state. Once a transaction succeeds, + * `apply()` flushes the buffer to the target `RawView` in a single pass. + * + * The `items_` map uses a `boost::container::pmr::monotonic_buffer_resource` + * with a 256 KB initial arena for O(1) amortised allocation during the burst + * of mutations that constitute a single transaction round. Because the + * resource cannot be shared or assigned, copy construction allocates a fresh + * resource and deep-copies the map; move construction transfers the + * `unique_ptr` directly. Both assignment operators are deleted. + * + * XRP fee destruction is tracked separately in `dropsDestroyed_` and + * replayed as a single `rawDestroyXRP` call during `apply()`. + * + * @note This class is an internal implementation detail of `OpenView`. + * Transaction logic should not interact with it directly; use the + * `RawView` interface instead. + * @see OpenView, RawView + */ class RawStateTable { public: using key_type = ReadView::key_type; - // Initial size for the monotonic_buffer_resource used for allocations - // The size was chosen from the old `qalloc` code (which this replaces). - // It is unclear how the size initially chosen in qalloc. + + /** Initial arena size for the PMR monotonic buffer resource. + * + * Inherited from the legacy `qalloc` scheme this replaced. The 256 KB + * budget covers the typical per-transaction working set without triggering + * heap growth for the common case. + */ static constexpr size_t kINITIAL_BUFFER_SIZE = kilobytes(256); + /** Construct an empty table with a fresh 256 KB monotonic arena. */ RawStateTable() : monotonic_resource_{std::make_unique( kINITIAL_BUFFER_SIZE)} , items_{monotonic_resource_.get()} {}; + /** Copy-construct by allocating a fresh monotonic arena and copying items. + * + * The SLE `shared_ptr` values in `items_` are shared with the source — + * not deep-copied — which is safe because SLEs are immutable once + * published. `dropsDestroyed_` is copied verbatim. + * + * @param rhs The source table to copy. + */ RawStateTable(RawStateTable const& rhs) : monotonic_resource_{std::make_unique( kINITIAL_BUFFER_SIZE)} , items_{rhs.items_, monotonic_resource_.get()} , dropsDestroyed_{rhs.dropsDestroyed_} {}; + /** Move-construct by transferring the monotonic resource and items map. + * + * After the move, the source table is left in a valid but empty state. + * The `unique_ptr` transfer preserves the stable address that `items_`' + * `polymorphic_allocator` holds. + */ RawStateTable(RawStateTable&&) = default; RawStateTable& @@ -39,48 +85,166 @@ public: RawStateTable& operator=(RawStateTable const&) = delete; + /** Flush all buffered mutations to a backing `RawView`. + * + * First calls `to.rawDestroyXRP(dropsDestroyed_)` to replay accumulated + * fee burns, then iterates `items_` and dispatches each pending action + * to the corresponding `rawErase`, `rawInsert`, or `rawReplace` method. + * The table is not cleared after apply; this object should be discarded + * or destroyed once flushed. + * + * @param to The target `RawView` that receives all buffered mutations. + */ void apply(RawView& to) const; + /** Test whether an SLE exists, overlaying the pending delta onto `base`. + * + * Checks the pending buffer first: a pending erase returns `false`; a + * pending insert or replace returns `true` only if `k.check()` passes + * (type-tag validation). Falls through to `base.exists(k)` when the key + * has no pending action. + * + * @param base The underlying read-only ledger state. + * @param k The keylet specifying key and expected SLE type. + * @return `true` if the entry exists and its type satisfies `k.check()`. + */ [[nodiscard]] bool exists(ReadView const& base, Keylet const& k) const; + /** Find the smallest key strictly greater than `key` in the merged state. + * + * Runs two parallel searches: (1) walks `base.succ()` repeatedly, + * skipping any base key that has a pending `Action::Erase`; (2) scans + * `items_` forward from `key` for the first non-erase entry. Returns + * the lower of the two candidates. If `last` is given and the result is + * `>= last`, returns `std::nullopt` (half-open range semantics). + * + * @param base The underlying read-only ledger state. + * @param key Exclusive lower bound; the search begins strictly after this. + * @param last Optional exclusive upper bound; `std::nullopt` means unbounded. + * @return The next existing key, or `std::nullopt` if none is in range. + */ [[nodiscard]] std::optional succ(ReadView const& base, key_type const& key, std::optional const& last) const; + /** Stage an SLE deletion, applying state-machine transition rules. + * + * Transitions on the key's existing pending action: + * - None → records `Action::Erase`. + * - `Insert` → removes the entry entirely (net-zero; base is unaffected). + * - `Replace` → downgrades to `Action::Erase`. + * - `Erase` → `LogicError` (double-delete). + * + * @param sle The ledger entry to stage for deletion; key is taken from the SLE. + * @throws std::logic_error if the key already has a pending erase. + */ void erase(std::shared_ptr const& sle); + /** Stage an SLE creation, applying state-machine transition rules. + * + * Transitions on the key's existing pending action: + * - None → records `Action::Insert`. + * - `Erase` → upgrades to `Action::Replace` (delete-then-recreate in + * the same transaction batch). + * - `Insert` → `LogicError` (duplicate insert). + * - `Replace` → `LogicError` (key already present in the delta). + * + * @param sle The new ledger entry to stage; key is taken from the SLE. + * @throws std::logic_error if the key is already pending insert or replace. + */ void insert(std::shared_ptr const& sle); + /** Stage an SLE field update, applying state-machine transition rules. + * + * Transitions on the key's existing pending action: + * - None → records `Action::Replace`. + * - `Insert` → updates the stored SLE pointer; preserves `Insert` + * because from the base's perspective the key is still being created. + * - `Replace` → updates the stored SLE pointer. + * - `Erase` → `LogicError` (cannot replace a deleted key). + * + * @param sle The updated ledger entry to stage; key is taken from the SLE. + * @throws std::logic_error if the key has a pending erase. + */ void replace(std::shared_ptr const& sle); + /** Read an SLE, overlaying the pending delta onto `base`. + * + * Checks the buffer first: a pending erase returns `nullptr`; a pending + * insert or replace returns the buffered SLE if `k.check()` passes + * (guards against type mismatches at the same key). Falls through to + * `base.read(k)` when the key has no pending action. + * + * @param base The underlying read-only ledger state. + * @param k The keylet specifying key and expected SLE type. + * @return The SLE if it exists and the type matches, otherwise `nullptr`. + */ [[nodiscard]] std::shared_ptr read(ReadView const& base, Keylet const& k) const; + /** Accumulate XRP drops to destroy at `apply()` time. + * + * Drops are not forwarded individually; they accumulate in + * `dropsDestroyed_` and are replayed as a single `rawDestroyXRP` call in + * `apply()`, keeping fee-burn accounting atomic with the rest of the flush. + * + * @param fee The quantity of XRP drops to add to the accumulated burn total. + */ void destroyXRP(XRPAmount const& fee); + /** Return a begin iterator for the merged SLE range over `base` and the delta. + * + * The returned iterator implements the two-pointer merge defined by + * `SlesIterImpl`: pending inserts appear in sorted position, pending + * erases are hidden, and pending replaces shadow the base entry. + * + * @param base The underlying read-only ledger state to merge with. + * @return A heap-allocated `iter_base` positioned at the first merged SLE. + */ [[nodiscard]] std::unique_ptr slesBegin(ReadView const& base) const; + /** Return an end sentinel for the merged SLE range over `base` and the delta. + * + * @param base The underlying read-only ledger state to merge with. + * @return A heap-allocated `iter_base` positioned past the last merged SLE. + */ [[nodiscard]] std::unique_ptr slesEnd(ReadView const& base) const; + /** Return an iterator to the first merged SLE with key strictly greater + * than `key`. + * + * @param base The underlying read-only ledger state to merge with. + * @param key Exclusive lower bound for the search. + * @return A heap-allocated `iter_base` positioned at the first qualifying SLE. + */ [[nodiscard]] std::unique_ptr slesUpperBound(ReadView const& base, uint256 const& key) const; private: + /** Pending mutation kind for an entry in `items_`. */ enum class Action { - Erase, - Insert, - Replace, + Erase, /**< Entry is scheduled for deletion. */ + Insert, /**< Entry is being created; does not yet exist in the base. */ + Replace, /**< Entry exists in the base and has been modified. */ }; + /** Private iterator class that merges base-view SLEs with the pending + * delta; defined in the `.cpp`. */ class SlesIterImpl; + /** Pairs a pending `Action` with the SLE it acts on. + * + * Stored as the mapped value in `items_`. The SLE pointer is always + * non-null; for `Erase` it is the last version written before the + * deletion was staged (used by `RawView::rawErase`). + */ struct SleAction { Action action; @@ -99,11 +263,17 @@ private: SleAction, std::less, boost::container::pmr::polymorphic_allocator>>; + // monotonic_resource_ must outlive `items_`. Make a pointer so it may be // easily moved. std::unique_ptr monotonic_resource_; + + /** Ordered map from ledger key to pending mutation; backed by the + * monotonic arena for O(1) amortised node allocation. */ items_t items_; + /** Accumulated XRP drops burned by fees; replayed as one `rawDestroyXRP` + * call during `apply()`. */ XRPAmount dropsDestroyed_{0}; }; diff --git a/include/xrpl/ledger/detail/ReadViewFwdRange.h b/include/xrpl/ledger/detail/ReadViewFwdRange.h index c548ccb101..381f46c844 100644 --- a/include/xrpl/ledger/detail/ReadViewFwdRange.h +++ b/include/xrpl/ledger/detail/ReadViewFwdRange.h @@ -1,3 +1,13 @@ +/** @file + * Type-erased forward-iterator infrastructure for `ReadView` traversal. + * + * Defines `ReadViewFwdIter` (the abstract iterator interface) and + * `ReadViewFwdRange` (the STL-compatible range wrapper) that together let + * any `ReadView` subclass expose its state and transaction maps through a + * single, stable iterator type. Callers interact indirectly via + * `ReadView::sles` and `ReadView::txs`; this header is internal plumbing. + */ + #pragma once #include @@ -10,8 +20,18 @@ class ReadView; namespace detail { -// A type-erased ForwardIterator -// +/** Abstract base defining the four primitive operations of a type-erased forward iterator. + * + * Each concrete `ReadView` implementation provides a private subclass of + * this template and hands heap-allocated instances to `ReadViewFwdRange::Iterator` + * via the factory methods `slesBegin()`, `slesEnd()`, `slesUpperBound()`, + * `txsBegin()`, and `txsEnd()` on `ReadView`. Callers never interact with + * this class directly. + * + * @tparam ValueType The element type yielded by the iterator — + * `std::shared_ptr` for state-map iteration or + * `ReadView::tx_type` for transaction-map iteration. + */ template class ReadViewFwdIter { @@ -27,21 +47,57 @@ public: virtual ~ReadViewFwdIter() = default; + /** Returns a heap-allocated deep copy of this iterator. + * + * Provides value-semantics copy for the owning `unique_ptr` wrapper. + * Each concrete subclass must return a new instance of itself in the + * same position. + * + * @return A `unique_ptr` to a fresh copy of this iterator instance. + */ [[nodiscard]] virtual std::unique_ptr copy() const = 0; + /** Returns `true` if this iterator denotes the same position as @p impl. + * + * Both iterators must be over the same underlying view; mixing iterators + * from different views produces undefined behavior. + * + * @param impl The other iterator to compare against. + * @return `true` when both iterators point to the same element (or both + * are end sentinels). + */ [[nodiscard]] virtual bool equal(ReadViewFwdIter const& impl) const = 0; + /** Advances this iterator to the next element in the sequence. */ virtual void increment() = 0; + /** Returns the element at the current iterator position. + * + * @return The current `ValueType` value. The result is cached by the + * wrapping `Iterator` so repeated dereferences are inexpensive. + * @throw May throw if the underlying view operation fails. + */ [[nodiscard]] virtual value_type dereference() const = 0; }; -// A range using type-erased ForwardIterator -// +/** STL-compatible forward range backed by a type-erased iterator. + * + * Wraps a `ReadViewFwdIter` behind a regular value-type iterator + * so that callers can write range-for loops over any `ReadView` subclass + * without knowing the concrete iterator type. Virtual dispatch is hidden + * inside the `impl_` pointer; the public `Iterator` API is fully inlined. + * + * `ReadView::SlesType` and `ReadView::TxsType` inherit from this template; + * application code should use those types rather than instantiating + * `ReadViewFwdRange` directly. + * + * @tparam ValueType The element type — must be noexcept-move-constructible + * so that `Iterator` move operations are noexcept. + */ template class ReadViewFwdRange { @@ -53,6 +109,18 @@ public: "ReadViewFwdRange move and move assign constructors should be " "noexcept"); + /** STL forward iterator over a `ReadViewFwdRange`. + * + * Value-type wrapper around a heap-allocated `iter_base`. Copy uses + * `iter_base::copy()` for a polymorphic deep clone; move transfers + * ownership of the `unique_ptr` without allocation and is `noexcept`. + * Dereference results are cached in `cache_` and cleared on advance, + * amortizing the cost of repeated `*it` or `it->` calls in tight loops. + * + * @note Comparing iterators from different views triggers an + * `XRPL_ASSERT` in debug builds. The `view_` pointer is carried + * solely for this cross-view sanity check. + */ class Iterator { public: @@ -66,43 +134,127 @@ public: using iterator_category = std::forward_iterator_tag; + /** Constructs a singular (default) iterator. + * + * A default-constructed iterator is not dereferenceable and must + * not be incremented. It compares equal only to other + * default-constructed iterators. + */ Iterator() = default; + /** Copy-constructs an independent iterator at the same position. + * + * Calls `iter_base::copy()` to deep-clone the polymorphic + * implementation, producing a new iterator that advances + * independently of @p other. + * + * @param other The iterator to clone. + */ Iterator(Iterator const& other); + + /** Move-constructs an iterator, transferring ownership of the impl. + * + * @param other The iterator to move from; left in a valid but + * singular state. + */ Iterator(Iterator&& other) noexcept; - // Used by the implementation + /** Constructs an iterator from a raw view pointer and a polymorphic impl. + * + * Used exclusively by `ReadView`'s factory methods (`slesBegin()`, + * `slesEnd()`, etc.). Not intended for direct use by callers. + * + * @param view The owning view; stored only for cross-view assertion. + * @param impl The heap-allocated concrete iterator; ownership is + * transferred to this object. + */ explicit Iterator(ReadView const* view, std::unique_ptr impl); + /** Copy-assigns from another iterator at the same position. + * + * Deep-clones via `iter_base::copy()`. + * + * @param other The iterator to copy. + * @return `*this`. + */ Iterator& operator=(Iterator const& other); + /** Move-assigns from another iterator. + * + * @param other The iterator to move from; left in a valid but + * singular state. + * @return `*this`. + */ Iterator& operator=(Iterator&& other) noexcept; + /** Returns `true` if both iterators denote the same position. + * + * Delegates to `iter_base::equal()`. Two null `impl_` pointers also + * compare equal (both are end sentinels / default-constructed). + * + * @param other The iterator to compare against. + * @return `true` when both iterators are at the same element. + * @note Asserts in debug builds that both iterators belong to the + * same view. Comparing iterators from different views is + * undefined behaviour. + */ bool operator==(Iterator const& other) const; + /** Returns `true` if the iterators denote different positions. + * + * @param other The iterator to compare against. + * @return `true` when the iterators are not at the same element. + */ bool operator!=(Iterator const& other) const; + /** Returns a reference to the current element. + * + * The result is cached after the first call; subsequent calls before + * the next `operator++` return the cached value at no extra cost. + * + * @return A `const` reference to the current `ValueType`. + * @throw May throw if the underlying `iter_base::dereference()` call fails. + */ // Can throw reference operator*() const; + /** Returns a pointer to the current element. + * + * Delegates to `operator*()` so caching and exception behaviour are + * identical to that of the dereference operator. + * + * @return A `const` pointer to the current `ValueType`. + * @throw May throw if the underlying `iter_base::dereference()` call fails. + */ // Can throw pointer operator->() const; + /** Advances the iterator and clears the dereference cache. + * + * @return `*this` after advancing to the next element. + */ Iterator& operator++(); + /** Returns a copy of the current iterator, then advances. + * + * @return An iterator to the element before the advance. + */ Iterator operator++(int); private: + /** Owning view; compared in `operator==` to catch cross-view misuse. */ ReadView const* view_ = nullptr; + /** Heap-allocated polymorphic iterator; null for the end sentinel. */ std::unique_ptr impl_{}; + /** One-slot dereference cache; cleared on each advance. */ std::optional mutable cache_; }; @@ -118,11 +270,19 @@ public: ReadViewFwdRange& operator=(ReadViewFwdRange const&) = default; + /** Constructs a range bound to @p view. + * + * The range stores a raw pointer to the view. The view must outlive + * the range and any iterators derived from it. + * + * @param view The `ReadView` whose factory methods supply iterators. + */ explicit ReadViewFwdRange(ReadView const& view) : view_(&view) { } protected: + /** The view whose factory methods supply concrete `iter_base` instances. */ ReadView const* view_; }; diff --git a/include/xrpl/ledger/helpers/AMMHelpers.h b/include/xrpl/ledger/helpers/AMMHelpers.h index c62437bf75..d71bdd2a5d 100644 --- a/include/xrpl/ledger/helpers/AMMHelpers.h +++ b/include/xrpl/ledger/helpers/AMMHelpers.h @@ -1,3 +1,22 @@ +/** @file + * Mathematical and operational backbone of the XRPL Automated Market Maker. + * + * Provides every computation needed to run a constant-product AMM pool: + * LP token minting and burning (XLS-30d Equations 3, 4, 7, 8), spot-price + * quality alignment against the central limit order book, swap execution with + * rigorous directional rounding, and ledger-state helpers for pool balance + * queries and AMM account lifecycle management. + * + * All arithmetic observes the pool invariant: + * @code + * sqrt(poolAsset1 × poolAsset2) >= LPTokenBalance + * @endcode + * Rounding is always directed to keep the pool at least as large as required. + * The `fixAMMv1_1` amendment introduced per-step directional rounding for + * swaps; `fixAMMv1_3` extended this discipline to LP token and + * deposit/withdrawal formulas. Pre-amendment paths are preserved for + * historic ledger replay. + */ #pragma once #include @@ -22,6 +41,17 @@ namespace xrpl { namespace detail { +/** Scale @p amount down by 99.99% as a last-resort quality rescue. + * + * When the rounded offer from `getAMMOfferStartWithTakerGets` or + * `getAMMOfferStartWithTakerPays` still falls below the target quality due + * to XRP integer-drop discretization, this function shrinks it by 0.01% + * (rounding toward zero) so the resulting offer quality meets or exceeds + * the target without generating an implausibly small trade. + * + * @param amount The offer side (takerGets or takerPays) to reduce. + * @return The reduced amount, or zero if already at zero. + */ Number reduceOffer(auto const& amount) { @@ -34,22 +64,41 @@ reduceOffer(auto const& amount) } // namespace detail +/** Direction tag used throughout deposit/withdrawal and rounding helpers. + * + * Passed to functions that behave asymmetrically between deposit (LP tokens + * rounded down, assets rounded up) and withdrawal (LP tokens rounded up, + * assets rounded down) to preserve the pool invariant. + */ enum class IsDeposit : bool { No = false, Yes = true }; -/** Calculate LP Tokens given AMM pool reserves. - * @param asset1 AMM one side of the pool reserve - * @param asset2 AMM another side of the pool reserve - * @return LP Tokens as IOU +/** Compute the initial LP token supply for a newly seeded AMM pool. + * + * Uses the geometric mean `sqrt(asset1 × asset2)`, which sets the + * pool invariant to equality at creation: `sqrt(asset1 × asset2) == LPTokens`. + * Under `fixAMMv1_3` the result is rounded downward so the pool starts + * with a slight surplus, preserving the invariant. + * + * @param asset1 Balance of the first pool asset. + * @param asset2 Balance of the second pool asset. + * @param lptIssue Asset descriptor identifying the LP token currency/issuer. + * @return Initial LP token amount as an IOU `STAmount`. */ STAmount ammLPTokens(STAmount const& asset1, STAmount const& asset2, Asset const& lptIssue); -/** Calculate LP Tokens given asset's deposit amount. - * @param asset1Balance current AMM asset1 balance - * @param asset1Deposit requested asset1 deposit amount - * @param lptAMMBalance AMM LPT balance - * @param tfee trading fee in basis points - * @return tokens +/** LP tokens minted for a single-asset deposit (XLS-30d Equation 3). + * + * A single-sided deposit is economically equivalent to a proportional + * deposit plus a fee-bearing swap; the fee is embedded via `feeMult` and + * `feeMultHalf`. Under `fixAMMv1_3` the final multiplication is rounded + * downward so fewer tokens are issued, preserving the pool invariant. + * + * @param asset1Balance Current pool balance of the asset being deposited. + * @param asset1Deposit Amount being deposited. + * @param lptAMMBalance Current total LP token supply. + * @param tfee Trading fee in basis points (e.g. 1000 = 1%). + * @return LP tokens to mint for the depositor. */ STAmount lpTokensOut( @@ -58,12 +107,19 @@ lpTokensOut( STAmount const& lptAMMBalance, std::uint16_t tfee); -/** Calculate asset deposit given LP Tokens. - * @param asset1Balance current AMM asset1 balance - * @param lpTokens LP Tokens - * @param lptAMMBalance AMM LPT balance - * @param tfee trading fee in basis points - * @return +/** Asset deposit required to receive a given number of LP tokens (XLS-30d Equation 4). + * + * Inverse of `lpTokensOut`: solves Equation 3 for the deposit amount given a + * desired token output. The solution is a quadratic whose positive root is + * found via `solveQuadraticEq`. Under `fixAMMv1_3` the result is rounded + * upward so the depositor contributes slightly more, preserving the pool + * invariant. + * + * @param asset1Balance Current pool balance of the asset to deposit. + * @param lptAMMBalance Current total LP token supply. + * @param lpTokens Desired LP token amount. + * @param tfee Trading fee in basis points. + * @return Asset amount the depositor must contribute. */ STAmount ammAssetIn( @@ -72,13 +128,18 @@ ammAssetIn( STAmount const& lpTokens, std::uint16_t tfee); -/** Calculate LP Tokens given asset's withdraw amount. Return 0 - * if can't calculate. - * @param asset1Balance current AMM asset1 balance - * @param asset1Withdraw requested asset1 withdraw amount - * @param lptAMMBalance AMM LPT balance - * @param tfee trading fee in basis points - * @return tokens out amount +/** LP tokens to burn for a single-asset withdrawal (XLS-30d Equation 7). + * + * Computes how many LP tokens must be redeemed to withdraw a specified asset + * amount. Returns zero if the inputs make calculation impossible. Under + * `fixAMMv1_3` the final multiplication is rounded upward so more tokens must + * be burned, preserving the pool invariant. + * + * @param asset1Balance Current pool balance of the asset being withdrawn. + * @param asset1Withdraw Requested withdrawal amount. + * @param lptAMMBalance Current total LP token supply. + * @param tfee Trading fee in basis points. + * @return LP tokens the withdrawer must burn, or zero if the calculation fails. */ STAmount lpTokensIn( @@ -87,12 +148,18 @@ lpTokensIn( STAmount const& lptAMMBalance, std::uint16_t tfee); -/** Calculate asset withdrawal by tokens - * @param assetBalance balance of the asset being withdrawn - * @param lptAMMBalance total AMM Tokens balance - * @param lpTokens LP Tokens balance - * @param tfee trading fee in basis points - * @return calculated asset amount +/** Asset returned when burning a given number of LP tokens (XLS-30d Equation 8). + * + * Inverse of `lpTokensIn`: solves Equation 7 for the withdrawal amount given + * the token burn. Under `fixAMMv1_3` the final multiplication is rounded + * downward so the withdrawer receives slightly less, preserving the pool + * invariant. + * + * @param assetBalance Current pool balance of the asset to withdraw. + * @param lptAMMBalance Current total LP token supply. + * @param lpTokens LP tokens being burned. + * @param tfee Trading fee in basis points. + * @return Asset amount returned to the withdrawer. */ STAmount ammAssetOut( @@ -101,12 +168,19 @@ ammAssetOut( STAmount const& lpTokens, std::uint16_t tfee); -/** Check if the relative distance between the qualities - * is within the requested distance. - * @param calcQuality calculated quality - * @param reqQuality requested quality - * @param dist requested relative distance - * @return true if within dist, false otherwise +/** Check whether two `Quality` values are within a relative tolerance. + * + * `Quality` has no subtraction operator, so the comparison is performed via + * `Quality::rate()`, which returns the *inverse* of quality (output/input). + * The formula `(min.rate - max.rate) / min.rate < dist` is equivalent to + * the standard `(max - min) / max < dist` after accounting for the inversion. + * Used in `changeSpotPriceQuality` to suppress trace-level errors when the + * quality mismatch is within one part in ten million (1e-7). + * + * @param calcQuality Computed quality. + * @param reqQuality Target quality. + * @param dist Maximum acceptable relative distance (e.g. `Number(1, -7)`). + * @return `true` if the two qualities are within @p dist of each other. */ inline bool withinRelativeDistance(Quality const& calcQuality, Quality const& reqQuality, Number const& dist) @@ -120,12 +194,18 @@ withinRelativeDistance(Quality const& calcQuality, Quality const& reqQuality, Nu return ((min.rate() - max.rate()) / min.rate()) < dist; } -/** Check if the relative distance between the amounts - * is within the requested distance. - * @param calc calculated amount - * @param req requested amount - * @param dist requested relative distance - * @return true if within dist, false otherwise +/** Check whether two numeric amounts are within a relative tolerance. + * + * Computes `(max - min) / max` and tests that it is less than @p dist. + * Accepted for `STAmount`, `IOUAmount`, `XRPAmount`, `MPTAmount`, and + * `Number`. Used alongside the `Quality` overload to emit quality-mismatch + * errors only when the discrepancy is truly significant. + * + * @tparam Amt Amount type; constrained to the five types listed above. + * @param calc Computed amount. + * @param req Target amount. + * @param dist Maximum acceptable relative distance. + * @return `true` if the two amounts are within @p dist of each other. */ template requires( @@ -141,34 +221,49 @@ withinRelativeDistance(Amt const& calc, Amt const& req, Number const& dist) return ((max - min) / max) < dist; } -/** Solve quadratic equation to find takerGets or takerPays. Round - * to minimize the amount in order to maximize the quality. +/** Smallest positive root of `a·x² + b·x + c = 0`, used to minimize offer size. + * + * Uses the numerically stable "citardauq" formula (Blinn 2006): when `b > 0` + * it computes `2c / (-b - sqrt(d))` instead of the standard + * `(-b + sqrt(d)) / 2a`, avoiding catastrophic cancellation when the two + * terms in the numerator are nearly equal. Minimizing the root maximizes + * offer quality in `getAMMOfferStartWithTakerGets` / `getAMMOfferStartWithTakerPays`. + * + * @param a Quadratic coefficient. + * @param b Linear coefficient. + * @param c Constant term. + * @return The smallest positive root, or `std::nullopt` if the discriminant + * is negative (no real solution) or the root is non-positive. */ std::optional solveQuadraticEqSmallest(Number const& a, Number const& b, Number const& c); -/** Generate AMM offer starting with takerGets when AMM pool - * from the payment perspective is IOU(in)/XRP(out) - * Equations: - * Spot Price Quality after the offer is consumed: - * Qsp = (O - o) / (I + i) -- equation (1) - * where O is poolPays, I is poolGets, o is takerGets, i is takerPays - * Swap out: - * i = (I * o) / (O - o) * f -- equation (2) - * where f is (1 - tfee/100000), tfee is in basis points - * Effective price targetQuality: - * Qep = o / i -- equation (3) - * There are two scenarios to consider - * A) Qsp = Qep. Substitute i in (1) with (2) and solve for o - * and Qsp = targetQuality(Qt): - * o**2 + o * (I * Qt * (1 - 1 / f) - 2 * O) + O**2 - Qt * I * O = 0 - * B) Qep = Qsp. Substitute i in (3) with (2) and solve for o - * and Qep = targetQuality(Qt): - * o = O - I * Qt / f - * Since the scenario is not known a priori, both A and B are solved and - * the lowest value of o is takerGets. takerPays is calculated with - * swap out eq (2). If o is less or equal to 0 then the offer can't - * be generated. +/** Generate a synthetic AMM offer whose quality matches @p targetQuality, + * starting from takerGets (XRP out, IOU in). + * + * Used when the pool pays XRP (IOU-in / XRP-out). Starting from the XRP + * side ensures that rounding XRP down to integer drops improves rather than + * degrades offer quality (post-`fixAMMv1_1` behavior). + * + * Two binding constraints are solved and the smaller takerGets is chosen: + * - Scenario A — post-swap spot price equals @p targetQuality: + * `o² + o·(I·Qt·(1 - 1/f) - 2·O) + O² - Qt·I·O = 0` + * - Scenario B — effective offer price equals @p targetQuality: + * `o = O - I·Qt / f` + * + * where `O = poolPays`, `I = poolGets`, `f = feeMult(tfee)`. + * takerPays is then derived from the swap-out equation. If the resulting + * offer quality is still below @p targetQuality after rounding, a 99.99% + * rescale via `detail::reduceOffer` is attempted. + * + * @tparam TIn Asset type flowing into the pool (IOU side). + * @tparam TOut Asset type flowing out of the pool (XRP side). + * @param pool Current AMM pool balances (`in` = poolGets, `out` = poolPays). + * @param targetQuality Desired offer quality (CLOB best quality). + * @param tfee Trading fee in basis points. + * @return Seated `{takerPays, takerGets}` amounts, or `std::nullopt` if a + * valid offer cannot be generated (e.g. target quality unreachable at + * current fee). */ template std::optional> @@ -214,28 +309,30 @@ getAMMOfferStartWithTakerGets( return amounts; } -/** Generate AMM offer starting with takerPays when AMM pool - * from the payment perspective is XRP(in)/IOU(out) or IOU(in)/IOU(out). - * Equations: - * Spot Price Quality after the offer is consumed: - * Qsp = (O - o) / (I + i) -- equation (1) - * where O is poolPays, I is poolGets, o is takerGets, i is takerPays - * Swap in: - * o = (O * i * f) / (I + i * f) -- equation (2) - * where f is (1 - tfee/100000), tfee is in basis points - * Effective price quality: - * Qep = o / i -- equation (3) - * There are two scenarios to consider - * A) Qsp = Qep. Substitute o in (1) with (2) and solve for i - * and Qsp = targetQuality(Qt): - * i**2 * f + i * I * (1 + f) + I**2 - I * O / Qt = 0 - * B) Qep = Qsp. Substitute i in (3) with (2) and solve for i - * and Qep = targetQuality(Qt): - * i = O / Qt - I / f - * Since the scenario is not known a priori, both A and B are solved and - * the lowest value of i is takerPays. takerGets is calculated with - * swap in eq (2). If i is less or equal to 0 then the offer can't - * be generated. +/** Generate a synthetic AMM offer whose quality matches @p targetQuality, + * starting from takerPays (XRP in, or IOU/IOU). + * + * Used for XRP-in/IOU-out and IOU/IOU pools. Starting from the XRP + * side (takerPays) under `fixAMMv1_1` keeps rounding effects favorable. + * + * Two binding constraints are solved and the smaller takerPays is chosen: + * - Scenario A — post-swap spot price equals @p targetQuality: + * `i²·f + i·I·(1+f) + I² - I·O/Qt = 0` + * - Scenario B — effective offer price equals @p targetQuality: + * `i = O/Qt - I/f` + * + * where `O = poolPays`, `I = poolGets`, `f = feeMult(tfee)`. + * takerGets is then derived from the swap-in equation. If the resulting + * offer quality is still below @p targetQuality after rounding, a 99.99% + * rescale via `detail::reduceOffer` is attempted. + * + * @tparam TIn Asset type flowing into the pool. + * @tparam TOut Asset type flowing out of the pool. + * @param pool Current AMM pool balances (`in` = poolGets, `out` = poolPays). + * @param targetQuality Desired offer quality (CLOB best quality). + * @param tfee Trading fee in basis points. + * @return Seated `{takerPays, takerGets}` amounts, or `std::nullopt` if a + * valid offer cannot be generated. */ template std::optional> @@ -281,21 +378,34 @@ getAMMOfferStartWithTakerPays( return amounts; } -/** Generate AMM offer so that either updated Spot Price Quality (SPQ) - * 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 XRP, - * 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 XRP offer side first. The result - * is rounded down, which makes the offer quality better. - * 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 - * @param quality requested quality - * @param tfee trading fee in basis points - * @return seated in/out amounts if the quality can be changed +/** Generate a synthetic AMM offer that aligns the pool's spot price with a CLOB quality. + * + * The payment engine calls this when it encounters both AMM pools and order + * book offers for the same currency pair. The resulting offer has a quality + * such that either the post-swap spot price equals @p quality (AMM offer + * quality is better) or the offer's effective price equals @p quality (the + * post-swap spot price is better) — whichever produces the smaller offer. + * + * Amendment behavior: + * - Pre-`fixAMMv1_1`: always solves for takerPays first; rounding down XRP + * takerGets can push quality below target, causing the offer to be rejected. + * - Post-`fixAMMv1_1`: solves for the XRP side first (takerGets when pool pays + * XRP, takerPays otherwise) so XRP rounding improves rather than degrades + * quality. Falls back to `detail::reduceOffer` if quality is still below + * target after rounding. + * + * A quality mismatch larger than 1e-7 is logged at `j.error()` level; smaller + * mismatches are trace-only. + * + * @tparam TIn Asset type flowing into the pool. + * @tparam TOut Asset type flowing out of the pool. + * @param pool Current AMM pool balances. + * @param quality Target quality (best CLOB offer quality for this pair). + * @param tfee Trading fee in basis points. + * @param rules Current ledger rules (for amendment checks). + * @param j Journal for diagnostic logging. + * @return Seated `{takerPays, takerGets}` amounts, or `std::nullopt` if the + * quality cannot be achieved (generally at high fees). */ template std::optional> @@ -398,26 +508,26 @@ changeSpotPriceQuality( return amounts; } -/** AMM pool invariant - the product (A * B) after swap in/out has to remain - * at least the same: (A + in) * (B - out) >= A * B - * XRP round-off may result in a smaller product after swap in/out. - * To address this: - * - if on swapIn the out is XRP then the amount is round-off - * downward, making the product slightly larger since out - * value is reduced. - * - if on swapOut the in is XRP then the amount is round-off - * upward, making the product slightly larger since in - * value is increased. - */ +// --- Swap-in / Swap-out --- -/** Swap assetIn into the pool and swap out a proportional amount - * of the other asset. Implements AMM Swap in. - * @see [XLS30d:AMM - * Swap](https://github.com/XRPLF/XRPL-Standards/discussions/78) - * @param pool current AMM pool balances - * @param assetIn amount to swap in - * @param tfee trading fee in basis points - * @return +/** Deposit @p assetIn into the pool and receive a proportional amount of the + * other asset (AMM Swap in, XLS-30d). + * + * Formula: `out = pool.out - (pool.in × pool.out) / (pool.in + assetIn × feeMult(tfee))` + * + * Pool invariant: `(pool.in + assetIn) × (pool.out - out) >= pool.in × pool.out`. + * XRP integer rounding can violate this; post-`fixAMMv1_1` each sub-expression + * has an explicitly directed rounding mode so the pool retains a tiny surplus. + * The output is always rounded downward so the trader receives less, not more. + * + * @tparam TIn Asset type deposited (poolGets side). + * @tparam TOut Asset type received (poolPays side). + * @param pool Current AMM pool balances. + * @param assetIn Amount being deposited into the pool. + * @param tfee Trading fee in basis points. + * @return Amount of the output asset the trader receives; zero if the pool + * denominator is non-positive. + * @see [XLS-30d AMM Swap](https://github.com/XRPLF/XRPL-Standards/discussions/78) */ template TOut @@ -476,14 +586,23 @@ swapAssetIn(TAmounts const& pool, TIn const& assetIn, std::uint16_t t Number::RoundingMode::Downward); } -/** Swap assetOut out of the pool and swap in a proportional amount - * of the other asset. Implements AMM Swap out. - * @see [XLS30d:AMM - * Swap](https://github.com/XRPLF/XRPL-Standards/discussions/78) - * @param pool current AMM pool balances - * @param assetOut amount to swap out - * @param tfee trading fee in basis points - * @return +/** Withdraw @p assetOut from the pool and compute the required input asset (AMM Swap out, XLS-30d). + * + * Formula: `in = ((pool.in × pool.out) / (pool.out - assetOut) - pool.in) / feeMult(tfee)` + * + * The input is always rounded upward so the trader pays at least what the + * pool needs to maintain its invariant. Post-`fixAMMv1_1` each intermediate + * step is individually directed; if the pool denominator is non-positive (i.e. + * @p assetOut >= the entire pool), the maximum representable `TIn` is returned. + * + * @tparam TIn Asset type deposited (poolGets side). + * @tparam TOut Asset type withdrawn (poolPays side). + * @param pool Current AMM pool balances. + * @param assetOut Amount being withdrawn from the pool. + * @param tfee Trading fee in basis points. + * @return Amount of the input asset the trader must pay; `toMaxAmount` + * if the requested output would exhaust the pool. + * @see [XLS-30d AMM Swap](https://github.com/XRPLF/XRPL-Standards/discussions/78) */ template TIn @@ -542,35 +661,46 @@ swapAssetOut(TAmounts const& pool, TOut const& assetOut, std::uint16_ Number::RoundingMode::Upward); } -/** Return square of n. - */ +/** Return `n²`. */ Number square(Number const& n); -/** Adjust LP tokens to deposit/withdraw. - * Amount type keeps 16 digits. Maintaining the LP balance by adding - * deposited tokens or subtracting withdrawn LP tokens from LP balance - * results in losing precision in LP balance. I.e. the resulting LP balance - * is less than the actual sum of LP tokens. To adjust for this, subtract - * old tokens balance from the new one for deposit or vice versa for - * withdraw to cancel out the precision loss. - * @param lptAMMBalance LPT AMM Balance - * @param lpTokens LP tokens to deposit or withdraw - * @param isDeposit Yes if deposit, No if withdraw +/** Adjust LP tokens to account for 16-digit precision loss in the running balance. + * + * Adding newly-minted tokens to an already-large `lptAMMBalance` can lose + * significance in the least-significant digit: the stored balance advances + * by less than `lpTokens`. This function round-trips through the 16-digit + * representation by computing `(balance + tokens) - balance` (deposit) or + * `(tokens - balance) + balance` (withdraw), returning the value that will + * actually be committed to the ledger. Result is forced downward to ensure + * the adjusted tokens do not exceed the requested tokens. + * + * @param lptAMMBalance Current total LP token supply stored on the AMM SLE. + * @param lpTokens Tokens being minted or burned. + * @param isDeposit `IsDeposit::Yes` for deposit, `IsDeposit::No` for withdrawal. + * @return Adjusted token amount that exactly matches the representable delta + * in the 16-digit balance. */ STAmount adjustLPTokens(STAmount const& lptAMMBalance, STAmount const& lpTokens, IsDeposit isDeposit); -/** Calls adjustLPTokens() and adjusts deposit or withdraw amounts if - * the adjusted LP tokens are less than the provided LP tokens. - * @param amountBalance asset1 pool balance - * @param amount asset1 to deposit or withdraw - * @param amount2 asset2 to deposit or withdraw - * @param lptAMMBalance LPT AMM Balance - * @param lpTokens LP tokens to deposit or withdraw - * @param tfee trading fee in basis points - * @param isDeposit Yes if deposit, No if withdraw - * @return +/** Adjust deposit/withdrawal asset amounts to match the precision-corrected LP token count. + * + * Calls `adjustLPTokens()` to compute the representable token delta. If the + * adjusted count is less than @p lpTokens, the corresponding asset amounts are + * scaled down so the ledger does not grant assets that exceed what the LP token + * math supports. A no-op when `fixAMMv1_3` is active because `getRoundedLPTokens` + * already incorporates the precision adjustment. + * + * @param amountBalance Current pool balance of the primary asset. + * @param amount Primary asset amount to deposit or withdraw. + * @param amount2 Secondary asset amount for two-sided operations; `std::nullopt` + * for single-asset operations. + * @param lptAMMBalance Current total LP token supply. + * @param lpTokens Calculated LP tokens before precision adjustment. + * @param tfee Trading fee in basis points. + * @param isDeposit `IsDeposit::Yes` for deposit, `IsDeposit::No` for withdrawal. + * @return Tuple of `(adjustedAmount, adjustedAmount2, adjustedLPTokens)`. */ std::tuple, STAmount> adjustAmountsByLPTokens( @@ -582,17 +712,46 @@ adjustAmountsByLPTokens( std::uint16_t tfee, IsDeposit isDeposit); -/** Positive solution for quadratic equation: - * x = (-b + sqrt(b**2 + 4*a*c))/(2*a) +/** Positive root of `a·x² + b·x + c = 0` using the standard formula. + * + * Computes `x = (-b + sqrt(b² - 4·a·c)) / (2·a)`. Used by `ammAssetIn` + * to invert Equation 4; the discriminant is guaranteed non-negative by the + * deposit formula's domain. + * + * @param a Quadratic coefficient. + * @param b Linear coefficient. + * @param c Constant term. + * @return The positive root. */ Number solveQuadraticEq(Number const& a, Number const& b, Number const& c); +/** Multiply @p amount by @p frac with an explicitly directed rounding mode. + * + * Installs @p rm for both the `Number` multiplication and the subsequent + * `toSTAmount` conversion so that rounding is applied once at the final step, + * not accumulated through intermediates. This is the building block for all + * `fixAMMv1_3` directional-rounding paths. + * + * @param amount Base `STAmount` to scale. + * @param frac Scaling factor. + * @param rm Rounding mode to apply at the final conversion step. + * @return `amount × frac` rounded according to @p rm, expressed in the same + * asset as @p amount. + */ STAmount multiply(STAmount const& amount, Number const& frac, Number::RoundingMode rm); namespace detail { +/** Select the LP token rounding direction that preserves the pool invariant. + * + * Deposit: round downward (fewer tokens minted → pool worth more per token). + * Withdraw: round upward (more tokens burned → pool retains slightly more). + * + * @param isDeposit Direction of the operation. + * @return `Downward` for deposit, `Upward` for withdrawal. + */ inline Number::RoundingMode getLPTokenRounding(IsDeposit isDeposit) { @@ -602,6 +761,14 @@ getLPTokenRounding(IsDeposit isDeposit) : Number::RoundingMode::Upward; } +/** Select the asset rounding direction that preserves the pool invariant. + * + * Deposit: round upward (depositor pays slightly more → pool is larger). + * Withdraw: round downward (withdrawer receives slightly less → pool retains). + * + * @param isDeposit Direction of the operation. + * @return `Upward` for deposit, `Downward` for withdrawal. + */ inline Number::RoundingMode getAssetRounding(IsDeposit isDeposit) { @@ -613,10 +780,19 @@ getAssetRounding(IsDeposit isDeposit) } // namespace detail -/** Round AMM equal deposit/withdrawal amount. Deposit/withdrawal formulas - * calculate the amount as a fractional value of the pool balance. The rounding - * takes place on the last step of multiplying the balance by the fraction if - * AMMv1_3 is enabled. +/** Compute a proportional asset amount with amendment-gated directional rounding. + * + * Used for two-sided (equal) deposit/withdrawal where the asset amount is + * `balance × frac`. Under `fixAMMv1_3` the final multiplication is rounded + * via `detail::getAssetRounding` (upward on deposit, downward on withdraw). + * Without the amendment the result uses the current ambient rounding mode. + * + * @tparam A Type of @p frac; either `STAmount` or `Number`. + * @param rules Current ledger rules. + * @param balance Pool balance of the asset. + * @param frac Fraction of the pool balance to apply. + * @param isDeposit Direction; controls rounding when `fixAMMv1_3` is active. + * @return `balance × frac` rounded to preserve the pool invariant. */ template STAmount @@ -637,14 +813,20 @@ getRoundedAsset(Rules const& rules, STAmount const& balance, A const& frac, IsDe return multiply(balance, frac, rm); } -/** Round AMM single deposit/withdrawal amount. - * The lambda's are used to delay evaluation until the function - * is executed so that the calculation is not done twice. noRoundCb() is - * called if AMMv1_3 is disabled. Otherwise, the rounding is set and - * the amount is: - * isDeposit is Yes - the balance multiplied by productCb() - * isDeposit is No - the result of productCb(). The rounding is - * the same for all calculations in productCb() +/** Compute a single-asset deposit/withdrawal amount with amendment-gated rounding. + * + * The callback form defers evaluation to avoid computing the formula twice: + * - Without `fixAMMv1_3`: calls `noRoundCb()` and converts without directed rounding. + * - With `fixAMMv1_3`, deposit: calls `multiply(balance, productCb(), rm)`. + * - With `fixAMMv1_3`, withdrawal: installs @p rm globally and calls `productCb()` + * so every arithmetic step inside the callback shares the same rounding direction. + * + * @param rules Current ledger rules. + * @param noRoundCb Produces the unrounded result (pre-amendment path). + * @param balance Pool balance of the asset. + * @param productCb Produces the rounding fraction (post-amendment path). + * @param isDeposit Direction; controls which rounding mode is selected. + * @return Rounded asset amount preserving the pool invariant. */ STAmount getRoundedAsset( @@ -654,12 +836,18 @@ getRoundedAsset( std::function const& productCb, IsDeposit isDeposit); -/** Round AMM deposit/withdrawal LPToken amount. Deposit/withdrawal formulas - * calculate the lptokens as a fractional value of the AMM total lptokens. - * The rounding takes place on the last step of multiplying the balance by - * the fraction if AMMv1_3 is enabled. The tokens are then - * adjusted to factor in the loss in precision (we only keep 16 significant - * digits) when adding the lptokens to the balance. +/** Compute a proportional LP token amount with amendment-gated rounding and precision adjustment. + * + * Used for two-sided (equal) deposit/withdrawal. Under `fixAMMv1_3` the + * multiplication `balance × frac` is rounded via `detail::getLPTokenRounding`, + * then `adjustLPTokens` corrects for the 16-digit precision loss introduced + * when adding the result to the running LP token balance. + * + * @param rules Current ledger rules. + * @param balance Current total LP token supply. + * @param frac Fraction of the pool's LP supply to mint or burn. + * @param isDeposit Direction; controls rounding and sign of the adjustment. + * @return LP token amount after rounding and precision correction. */ STAmount getRoundedLPTokens( @@ -668,16 +856,22 @@ getRoundedLPTokens( Number const& frac, IsDeposit isDeposit); -/** Round AMM single deposit/withdrawal LPToken amount. - * The lambda's are used to delay evaluation until the function is executed - * so that the calculations are not done twice. - * noRoundCb() is called if AMMv1_3 is disabled. Otherwise, the rounding is set - * and the lptokens are: - * if isDeposit is Yes - the result of productCb(). The rounding is - * the same for all calculations in productCb() - * if isDeposit is No - the balance multiplied by productCb() - * The lptokens are then adjusted to factor in the loss in precision - * (we only keep 16 significant digits) when adding the lptokens to the balance. +/** Compute a single-asset LP token amount with amendment-gated rounding and precision adjustment. + * + * The callback form avoids evaluating the formula twice: + * - Without `fixAMMv1_3`: calls `noRoundCb()` with no directed rounding. + * - With `fixAMMv1_3`, deposit: installs the LP rounding mode globally and + * calls `productCb()` (all arithmetic inside shares the direction). + * - With `fixAMMv1_3`, withdrawal: calls `multiply(lptAMMBalance, productCb(), rm)`. + * In all post-amendment cases, `adjustLPTokens` then corrects for 16-digit + * precision loss in the running LP balance. + * + * @param rules Current ledger rules. + * @param noRoundCb Produces the unrounded result (pre-amendment path). + * @param lptAMMBalance Current total LP token supply. + * @param productCb Produces the rounding fraction (post-amendment path). + * @param isDeposit Direction; controls rounding mode selection. + * @return LP token amount after rounding and precision correction. */ STAmount getRoundedLPTokens( @@ -687,16 +881,21 @@ getRoundedLPTokens( std::function const& productCb, IsDeposit isDeposit); -/* Next two functions adjust asset in/out amount to factor in the adjusted - * lptokens. The lptokens are calculated from the asset in/out. The lptokens are - * then adjusted to factor in the loss in precision. The adjusted lptokens might - * be less than the initially calculated tokens. Therefore, the asset in/out - * must be adjusted. The rounding might result in the adjusted amount being - * greater than the original asset in/out amount. If this happens, - * then the original amount is reduced by the difference in the adjusted amount - * and the original amount. The actual tokens and the actual adjusted amount - * are then recalculated. The minimum of the original and the actual - * adjusted amount is returned. +/** Adjust a single-asset deposit amount to match the precision-corrected LP token count. + * + * Under `fixAMMv1_3`: computes `ammAssetIn(balance, lptAMMBalance, tokens, tfee)`. + * If rounding causes the derived asset amount to exceed @p amount, the deposit is + * reduced by the overshoot and both tokens and asset are recomputed, then the minimum + * of original and adjusted amounts is returned. Before the amendment, returns the + * inputs unchanged. + * + * @param rules Current ledger rules. + * @param balance Pool balance of the asset being deposited. + * @param amount Requested deposit amount. + * @param lptAMMBalance Current total LP token supply. + * @param tokens LP token count before precision adjustment. + * @param tfee Trading fee in basis points. + * @return `{adjustedTokens, adjustedAmount}` pair. */ std::pair adjustAssetInByTokens( @@ -706,6 +905,23 @@ adjustAssetInByTokens( STAmount const& lptAMMBalance, STAmount const& tokens, std::uint16_t tfee); + +/** Adjust a single-asset withdrawal amount to match the precision-corrected LP token count. + * + * Under `fixAMMv1_3`: computes `ammAssetOut(balance, lptAMMBalance, tokens, tfee)`. + * If rounding causes the derived asset amount to exceed @p amount, the withdrawal is + * reduced by the overshoot and both tokens and asset are recomputed, then the minimum + * of original and adjusted amounts is returned. Before the amendment, returns the + * inputs unchanged. + * + * @param rules Current ledger rules. + * @param balance Pool balance of the asset being withdrawn. + * @param amount Requested withdrawal amount. + * @param lptAMMBalance Current total LP token supply. + * @param tokens LP token count before precision adjustment. + * @param tfee Trading fee in basis points. + * @return `{adjustedTokens, adjustedAmount}` pair. + */ std::pair adjustAssetOutByTokens( Rules const& rules, @@ -715,8 +931,20 @@ adjustAssetOutByTokens( STAmount const& tokens, std::uint16_t tfee); -/** Find a fraction of tokens after the tokens are adjusted. The fraction - * is used to adjust equal deposit/withdraw amount. +/** Recompute the LP token fraction after precision adjustment. + * + * Under `fixAMMv1_3` the precision-adjusted token count may differ from the + * originally requested count, so the fraction `tokens / lptAMMBalance` must + * be recomputed from the adjusted value before it is used to scale equal + * deposit/withdrawal amounts. Returns @p frac unchanged when `fixAMMv1_3` + * is inactive (the precision adjustment has not yet been applied). + * + * @param rules Current ledger rules. + * @param lptAMMBalance Current total LP token supply. + * @param tokens Precision-adjusted LP token count. + * @param frac Original fraction before adjustment. + * @return Adjusted fraction `tokens / lptAMMBalance`, or @p frac if + * `fixAMMv1_3` is not active. */ Number adjustFracByTokens( @@ -725,7 +953,19 @@ adjustFracByTokens( STAmount const& tokens, Number const& frac); -/** Get AMM pool balances. +/** Read the AMM's current pool asset balances from the ledger. + * + * Delegates to `accountHolds` for each asset, respecting freeze and + * authorization policy. Does not read the LP token balance. + * + * @param view Ledger state to query. + * @param ammAccountID AccountID of the AMM's pseudo-account. + * @param asset1 First pool asset. + * @param asset2 Second pool asset. + * @param freezeHandling Whether to enforce freeze restrictions. + * @param authHandling Whether to enforce authorization restrictions. + * @param j Journal for diagnostic logging. + * @return `{balance1, balance2}` pair in the same asset order as the inputs. */ std::pair ammPoolHolds( @@ -737,9 +977,23 @@ ammPoolHolds( AuthHandling authHandling, beast::Journal const j); -/** Get AMM pool and LP token balances. If both optIssue are - * provided then they are used as the AMM token pair issues. - * Otherwise the missing issues are fetched from ammSle. +/** Read the AMM's pool balances and total LP token supply from the ledger. + * + * When both optional assets are provided they are validated against the AMM + * SLE's stored pair and used as the query order; providing only one resolves + * the counterpart from `ammSle`. If neither is provided, the canonical order + * from `ammSle` is used. An invalid asset pair (mismatched with the AMM SLE) + * indicates a corrupted AMM object and returns `tecAMM_INVALID_TOKENS`. + * + * @param view Ledger state to query. + * @param ammSle The AMM's `ltAMM` SLE. + * @param optAsset1 Optional first asset override. + * @param optAsset2 Optional second asset override. + * @param freezeHandling Whether to enforce freeze restrictions. + * @param authHandling Whether to enforce authorization restrictions. + * @param j Journal for diagnostic logging. + * @return `{balance1, balance2, lpTokenBalance}` on success, or + * `Unexpected(tecAMM_INVALID_TOKENS)` if the asset pair is invalid. */ Expected, TER> ammHolds( @@ -751,7 +1005,21 @@ ammHolds( AuthHandling authHandling, beast::Journal const j); -/** Get the balance of LP tokens. +/** Read an LP's token balance from its direct trustline with the AMM account. + * + * Intentionally bypasses `accountHolds` — that function would also check + * whether the AMM's underlying pool assets are frozen (under + * `fixFrozenLPTokenTransfer`), which is incorrect policy for LP token balance + * queries. Only the LP token trustline's own freeze flag is checked. + * Trust-line orientation: raw `sfBalance` is negated when `lpAccount > ammAccount`. + * + * @param view Ledger state to query. + * @param asset1 First pool asset (used to derive the LP token currency). + * @param asset2 Second pool asset. + * @param ammAccount AccountID of the AMM's pseudo-account (LP token issuer). + * @param lpAccount AccountID of the liquidity provider. + * @param j Journal for diagnostic logging. + * @return The LP's token balance, or zero if the trustline is absent or frozen. */ STAmount ammLPHolds( @@ -762,6 +1030,17 @@ ammLPHolds( AccountID const& lpAccount, beast::Journal const j); +/** Read an LP's token balance using the asset pair stored in @p ammSle. + * + * Convenience overload; extracts `sfAsset`, `sfAsset2`, and `sfAccount` from + * @p ammSle and delegates to the five-parameter `ammLPHolds`. + * + * @param view Ledger state to query. + * @param ammSle The AMM's `ltAMM` SLE. + * @param lpAccount AccountID of the liquidity provider. + * @param j Journal for diagnostic logging. + * @return The LP's token balance, or zero if the trustline is absent or frozen. + */ STAmount ammLPHolds( ReadView const& view, @@ -769,25 +1048,72 @@ ammLPHolds( AccountID const& lpAccount, beast::Journal const j); -/** Get AMM trading fee for the given account. The fee is discounted - * if the account is the auction slot owner or one of the slot's authorized - * accounts. +/** Get the effective AMM trading fee for @p account. + * + * Returns the auction slot's `sfDiscountedFee` if the slot is unexpired and + * @p account is either the slot owner or one of up to four authorized accounts; + * otherwise returns the AMM's global `sfTradingFee`. Expiration is compared + * against the ledger's `parentCloseTime` (the slot stores + * `parentCloseTime + TOTAL_TIME_SLOT_SECS` at creation, i.e. 24 hours). + * + * @param view Ledger state providing the current close time. + * @param ammSle The AMM's `ltAMM` SLE. + * @param account The account whose fee rate is needed. + * @return Fee rate in basis points (0–1000). */ std::uint16_t getTradingFee(ReadView const& view, SLE const& ammSle, AccountID const& account); -/** Returns total amount held by AMM for the given token. +/** Read the AMM account's raw pool-asset balance, bypassing balance hooks. + * + * Unlike `accountHolds`, this function does not invoke `balanceHookIOU` or + * `balanceHookMPT`, so the result is unaffected by `PaymentSandbox` + * deferred-credit accounting. Used when the AMM needs its own unmodified + * balance for math, not for payment routing. Returns zero if the trustline + * or MPToken object is absent or frozen. + * + * @param view Ledger state to query. + * @param ammAccountID AccountID of the AMM's pseudo-account. + * @param asset The pool asset to query (IOU, XRP, or MPT). + * @return The raw balance, or zero if unavailable. */ STAmount ammAccountHolds(ReadView const& view, AccountID const& ammAccountID, Asset const& asset); -/** Delete trustlines to AMM. If all trustlines are deleted then - * AMM object and account are deleted. Otherwise tecINCOMPLETE is returned. +/** Remove all ledger objects owned by the AMM and, if successful, delete the AMM itself. + * + * Deletion is ordered: IOU trustlines first, then MPToken objects, then the + * AMM SLE and its `AccountRoot`. Because each ledger transaction has a bounded + * work budget, not all trustlines may be removable in one call; in that case + * `tecINCOMPLETE` is returned and the caller must submit additional transactions + * to finish. The AMM can be re-deposited while deletion is incomplete. + * + * @param view Sandbox for applying state changes. + * @param asset First pool asset (used to locate the AMM keylet). + * @param asset2 Second pool asset. + * @param j Journal for diagnostic logging. + * @return `tesSUCCESS` on full deletion, `tecINCOMPLETE` if trustlines remain, + * or `tecINTERNAL` for unexpected ledger inconsistencies. */ TER deleteAMMAccount(Sandbox& view, Asset const& asset, Asset const& asset2, beast::Journal j); -/** Initialize Auction and Voting slots and set the trading/discounted fee. +/** Initialize the vote slot and auction slot on a new or re-created AMM. + * + * Called on both `AMMCreate` and on `AMMDeposit` when the pool was previously + * drained to zero. Sets up: + * - One vote entry for @p account with full weight (`kVOTE_WEIGHT_SCALE_FACTOR`). + * - An auction slot owned by @p account, expiring in 24 hours, at zero price. + * - `sfDiscountedFee` = `tfee / kAUCTION_SLOT_DISCOUNTED_FEE_FRACTION`. + * - Absent-field canonicalization: fee fields are removed if their value is zero. + * - Under `fixCleanup3_2_0`, stale `sfAuthAccounts` from any previous slot owner + * are cleared. + * + * @param view Apply-view for the current transaction. + * @param ammSle The AMM's `ltAMM` SLE (modified in place). + * @param account The creator/re-depositor receiving the slot. + * @param lptAsset The LP token asset descriptor (used as the `sfPrice` currency). + * @param tfee Trading fee in basis points to set. */ void initializeFeeAuctionVote( @@ -797,16 +1123,41 @@ initializeFeeAuctionVote( Asset const& lptAsset, std::uint16_t tfee); -/** Return true if the Liquidity Provider is the only AMM provider, false - * otherwise. Return tecINTERNAL if encountered an unexpected condition, - * for instance Liquidity Provider has more than one LPToken trustline. +/** Determine whether @p lpAccount is the sole remaining liquidity provider. + * + * Walks the AMM account's owner directory (up to 10 pages, covering at most + * 4 objects) counting LPToken trustlines, pool-asset trustlines, MPToken + * objects, and the AMM SLE itself. Any second LPToken trustline belonging to + * a different account returns `false` immediately. + * + * @param view Ledger state to query. + * @param ammIssue The LP token issue (currency + AMM account as issuer). + * @param lpAccount AccountID of the candidate sole LP. + * @return `true` if @p lpAccount is the only LP, `false` if other LPs exist, + * or `Unexpected(tecINTERNAL)` for any unexpected directory state + * (e.g. more than one LPToken trustline for @p lpAccount). */ Expected isOnlyLiquidityProvider(ReadView const& view, Issue const& ammIssue, AccountID const& lpAccount); -/** Due to rounding, the LPTokenBalance of the last LP might - * not match the LP's trustline balance. If it's within the tolerance, - * update LPTokenBalance to match the LP's trustline balance. +/** Reconcile the AMM's `sfLPTokenBalance` with the last LP's trustline balance. + * + * Accumulated rounding over the life of the pool can cause the AMM's running + * `sfLPTokenBalance` to differ slightly from the sole LP's trustline balance. + * This function: + * 1. Confirms @p account is the only remaining LP via `isOnlyLiquidityProvider`. + * 2. If so, verifies the discrepancy is within 0.1% (tolerance `1e-3`). + * 3. If within tolerance, updates `sfLPTokenBalance` to @p lpTokens so the + * final withdrawal leaves the AMM in a fully consistent state. + * + * @param sb Sandbox for applying the balance correction. + * @param lpTokens The last LP's actual trustline balance. + * @param ammSle The AMM's `ltAMM` SLE (updated in place if correction applied). + * @param account AccountID of the candidate sole LP. + * @return `true` if the balance was reconciled or no adjustment was needed + * (other LPs exist), `Unexpected(tecAMM_INVALID_TOKENS)` if the + * discrepancy exceeds tolerance, or `Unexpected(tecINTERNAL)` on an + * unexpected directory error. */ Expected verifyAndAdjustLPTokenBalance( diff --git a/include/xrpl/ledger/helpers/AccountRootHelpers.h b/include/xrpl/ledger/helpers/AccountRootHelpers.h index 353c27fe41..3d225c8c1c 100644 --- a/include/xrpl/ledger/helpers/AccountRootHelpers.h +++ b/include/xrpl/ledger/helpers/AccountRootHelpers.h @@ -1,3 +1,12 @@ +/** @file + * Free functions for querying and mutating `ltACCOUNT_ROOT` ledger entries. + * + * Provides the canonical helpers for freeze-state queries, spendable XRP + * balance, owner-count bookkeeping, transfer fees, destination-tag + * enforcement, and the creation and detection of pseudo-accounts (AMM, + * Vault, LoanBroker). Almost every transaction processor depends on at + * least one function here. + */ #pragma once #include @@ -15,26 +24,60 @@ namespace xrpl { -/** Check if the issuer has the global freeze flag set. - @param issuer The account to check - @return true if the account has global freeze set -*/ +/** Check whether an IOU issuer has the global freeze flag active. + * + * XRP is never frozen; this function returns `false` immediately for the XRP + * account. For any other issuer it reads `lsfGlobalFreeze` from the + * account root. Missing accounts are treated as non-frozen. + * + * @param view The read-only ledger view to query. + * @param issuer The account whose freeze state is to be checked. + * @return `true` if `issuer` is a non-XRP account with `lsfGlobalFreeze` set; + * `false` otherwise. + */ [[nodiscard]] bool isGlobalFrozen(ReadView const& view, AccountID const& issuer); -// Calculate liquid XRP balance for an account. -// This function may be used to calculate the amount of XRP that -// the holder is able to freely spend. It subtracts reserve requirements. -// -// ownerCountAdj adjusts the owner count in case the caller calculates -// before ledger entries are added or removed. Positive to add, negative -// to subtract. -// -// @param ownerCountAdj positive to add to count, negative to reduce count. +/** Compute the spendable XRP balance for an account after reserve deduction. + * + * Queries the account's current balance and owner count through the view's + * virtual hook methods (`balanceHookIOU`, `ownerCountHook`) so that + * `PaymentSandbox` can overlay uncommitted in-flight changes without any + * branching here. The reserve is then subtracted; if the balance is below + * the reserve, the function returns zero rather than a negative amount. + * + * Pseudo-accounts (AMM, Vault, LoanBroker) bypass the reserve calculation + * entirely and receive the full balance as spendable XRP, because they + * cannot submit transactions and must never be blocked by reserve checks. + * + * @param view The ledger view to query. + * @param id The account whose liquid XRP balance is computed. + * @param ownerCountAdj Signed delta applied to `sfOwnerCount` before the + * reserve is calculated. Pass a positive value when the caller is about + * to add ledger entries; pass a negative value when entries are about to + * be removed. This lets callers reason about post-mutation availability + * before the state is committed to the view. + * @param j Journal for trace-level diagnostics. + * @return The spendable XRP amount, clamped to zero from below. + */ [[nodiscard]] XRPAmount xrpLiquid(ReadView const& view, AccountID const& id, std::int32_t ownerCountAdj, beast::Journal j); -/** Adjust the owner count up or down. */ +/** Increment or decrement `sfOwnerCount` on an account SLE and notify the view. + * + * Delegates to a file-static helper that clamps the result to + * `[0, UINT32_MAX]`, logging at `fatal` severity if either bound would be + * exceeded — silent wrapping of the `uint32_t` field would corrupt ledger + * state. After clamping, `view.adjustOwnerCountHook()` is called before the + * new value is written; `PaymentSandbox` overrides that hook to track the + * high-water-mark count, ensuring subsequent `ownerCountHook` reads use the + * most conservative value seen during the payment. + * + * @param view The mutable view on which the SLE update is recorded. + * @param sle The account SLE to adjust; a null pointer is silently ignored. + * @param amount Signed delta to apply to `sfOwnerCount`; must be non-zero. + * @param j Journal for fatal-level diagnostics on overflow or underflow. + */ void adjustOwnerCount( ApplyView& view, @@ -42,45 +85,89 @@ adjustOwnerCount( std::int32_t amount, beast::Journal j); -/** Returns IOU issuer transfer fee as Rate. Rate specifies - * the fee as fractions of 1 billion. For example, 1% transfer rate - * is represented as 1,010,000,000. - * @param issuer The IOU issuer +/** Return the IOU transfer fee for an issuer as a `Rate` value. + * + * `Rate` expresses the fee as a fraction of one billion, so a 1% fee is + * represented as 1,010,000,000. If the issuer account does not exist or + * has not set `sfTransferRate`, `parityRate` (no fee, i.e., 1,000,000,000) + * is returned — callers never need to handle a null case. + * + * @param view The ledger view to query. + * @param issuer The IOU issuer whose transfer fee is requested. + * @return The issuer's `Rate`, or `parityRate` if none is configured. */ [[nodiscard]] Rate transferRate(ReadView const& view, AccountID const& issuer); -/** Generate a pseudo-account address from a pseudo owner key. - @param pseudoOwnerKey The key to generate the address from - @return The generated account ID -*/ +/** Derive a collision-free pseudo-account `AccountID` from an owner key. + * + * Iterates up to 256 attempts. Each attempt hashes a counter, the parent + * ledger's hash, and `pseudoOwnerKey` through `sha512Half` then + * `ripesha_hasher` (RIPEMD-160(SHA-256(...))). The parent-hash component + * prevents precomputation of collisions. The first candidate address that + * has no existing `AccountRoot` in `view` is returned. + * + * @param view The ledger view used to check for address collisions. + * @param pseudoOwnerKey The 256-bit key identifying the pseudo-account owner + * (e.g., the AMM or Vault object ID). + * @return A collision-free `AccountID`, or `beast::kZERO` if all 256 + * attempts collided. `createPseudoAccount` propagates exhaustion as + * `tecDUPLICATE`. + * @note The 256-attempt cap is consensus-critical and must not be changed + * without an amendment, as it determines the pseudo-account address space. + */ AccountID pseudoAccountAddress(ReadView const& view, uint256 const& pseudoOwnerKey); -/** Returns the list of fields that define an ACCOUNT_ROOT as a pseudo-account - if set. - - The list is constructed during initialization and is const after that. - Pseudo-account designator fields MUST be maintained by including the - SField::sMD_PseudoAccount flag in the SField definition. -*/ +/** Return the singleton list of `SField`s that designate a pseudo-account. + * + * Built once at first call by scanning the `ltACCOUNT_ROOT` `SOTemplate` + * from `LedgerFormats` and selecting every field whose `SField::sMD_PseudoAccount` + * metadata bit is set. Currently includes `sfAMMID`, `sfVaultID`, and + * `sfLoanBrokerID`. The discovery is fully data-driven: adding a new + * pseudo-account type requires only tagging its key field with + * `SField::sMD_PseudoAccount` in `sfields.macro` — no manual registration + * here is needed. + * + * @return A const reference to the cached vector of pseudo-account fields. + * @note Non-active amendments are harmless: the corresponding field will + * never be set in practice, so the list remains correct regardless of + * which amendments are enabled. + */ [[nodiscard]] std::vector const& getPseudoAccountFields(); -/** Returns true if and only if sleAcct is a pseudo-account or specific - pseudo-accounts in pseudoFieldFilter. - - Returns false if sleAcct is: - - NOT a pseudo-account OR - - NOT a ltACCOUNT_ROOT OR - - null pointer -*/ +/** Determine whether an SLE is a pseudo-account (optionally of a specific type). + * + * Returns `true` only when all three conditions hold: `sleAcct` is non-null, + * its ledger-entry type is `ltACCOUNT_ROOT`, and at least one pseudo-account + * designator field (from `getPseudoAccountFields()`) is present. When + * `pseudoFieldFilter` is non-empty, only fields in the filter are considered, + * allowing callers to distinguish AMM pseudo-accounts from Vault + * pseudo-accounts. + * + * @param sleAcct The SLE to inspect; may be null. + * @param pseudoFieldFilter Optional subset of pseudo-account fields to match + * against. An empty set (the default) matches any pseudo-account field. + * @return `true` if `sleAcct` is a pseudo-account (of a type in the filter + * when one is provided); `false` otherwise. + */ [[nodiscard]] bool isPseudoAccount( std::shared_ptr sleAcct, std::set const& pseudoFieldFilter = {}); -/** Convenience overload that reads the account from the view. */ +/** Convenience overload that looks up the account from a `ReadView`. + * + * Reads the `AccountRoot` for `accountId` via `keylet::account()` and + * delegates to the SLE overload. + * + * @param view The ledger view to query. + * @param accountId The account address to look up. + * @param pseudoFieldFilter Optional field filter forwarded to the SLE overload. + * @return `true` if the account exists and is a pseudo-account matching the + * filter; `false` otherwise. + */ [[nodiscard]] inline bool isPseudoAccount( ReadView const& view, @@ -90,22 +177,48 @@ isPseudoAccount( return isPseudoAccount(view.read(keylet::account(accountId)), pseudoFieldFilter); } -/** - * Create pseudo-account, storing pseudoOwnerKey into ownerField. +/** Create a protocol-owned pseudo-account `AccountRoot` SLE. * - * The list of valid ownerField is maintained in AccountRootHelpers.cpp and - * the caller to this function must perform necessary amendment check(s) - * before using a field. The amendment check is **not** performed in - * createPseudoAccount. + * Derives a collision-free address via `pseudoAccountAddress()`, constructs + * an `AccountRoot` with zero balance, `lsfDisableMaster | lsfDefaultRipple | + * lsfDepositAuth`, and stores `pseudoOwnerKey` in `ownerField`. When + * `featureSingleAssetVault` or `featureLendingProtocol` is enabled, + * `sfSequence` is set to `0`; otherwise it is set to the current ledger + * sequence. The zero sequence makes pseudo-accounts visually distinguishable + * and provides an extra barrier against accidental transaction submission. + * + * In debug builds, an `XRPL_ASSERT` fires if `ownerField` does not carry the + * `SField::sMD_PseudoAccount` flag, catching misuse at development time. + * + * @param view The mutable ledger view into which the new SLE is + * inserted. + * @param pseudoOwnerKey The 256-bit key of the owning object (e.g., the AMM + * or Vault ledger entry key); stored in `ownerField` on the new SLE. + * @param ownerField The back-link field written on the new SLE; must be + * one of the fields returned by `getPseudoAccountFields()`. + * @return The newly created SLE on success, or `tecDUPLICATE` if all 256 + * address derivation attempts collided. + * @note Amendment checks are the **caller's** responsibility. This function + * is amendment-neutral by design; callers such as `VaultCreate` and + * `LoanBrokerSet` must gate on the relevant feature flag before invoking. */ [[nodiscard]] Expected, TER> createPseudoAccount(ApplyView& view, uint256 const& pseudoOwnerKey, SField const& ownerField); -/** Checks the destination and tag. - - - Checks that the SLE is not null. - - If the SLE requires a destination tag, checks that there is a tag. -*/ +/** Validate a payment destination SLE and its destination-tag requirement. + * + * Returns `tecNO_DST` if `toSle` is null (the destination account does not + * exist), and `tecDST_TAG_NEEDED` if the destination has set + * `lsfRequireDestTag` but the transaction supplies no tag. Returns + * `tesSUCCESS` otherwise. + * + * @param toSle The destination account SLE; may be null. + * @param hasDestinationTag `true` if the transaction includes a destination + * tag field. + * @return `tecNO_DST`, `tecDST_TAG_NEEDED`, or `tesSUCCESS`. + * @note The ledger enforces the *presence* of a tag but never interprets its + * value; semantics (e.g., exchange user IDs) are opaque to the protocol. + */ [[nodiscard]] TER checkDestinationAndTag(SLE::const_ref toSle, bool hasDestinationTag); diff --git a/include/xrpl/ledger/helpers/CredentialHelpers.h b/include/xrpl/ledger/helpers/CredentialHelpers.h index e06d225934..23dd19a9fd 100644 --- a/include/xrpl/ledger/helpers/CredentialHelpers.h +++ b/include/xrpl/ledger/helpers/CredentialHelpers.h @@ -1,3 +1,17 @@ +/** @file + * Central contract for credential and deposit pre-authorization logic. + * + * Included by every fund-transfer transactor (Payment, EscrowFinish, + * PaymentChannelClaim, VaultDeposit) that must honor destination-account + * access controls. + * + * Functions divide along the preclaim / doApply boundary: + * - `xrpl::credentials::*` — read-only checks safe to call from preclaim. + * - `xrpl::verifyDepositPreauth` / `xrpl::verifyValidDomain` — mutating + * counterparts that must be called from doApply when the corresponding + * preclaim function succeeds, so that expired credential objects are + * physically deleted from the ledger as a side effect. + */ #pragma once #include @@ -13,57 +27,225 @@ namespace xrpl { namespace credentials { -// These function will be used by the code that use DepositPreauth / Credentials -// (and any future pre-authorization modes) as part of authorization (all the -// transfer funds transactions) - -// Check if credential sfExpiration field has passed ledger's parentCloseTime +/** Test whether a credential SLE has passed its expiration time. + * + * Reads `sfExpiration` from @p sleCredential, defaulting to + * `std::numeric_limits::max()` when the field is absent, so + * credentials with no expiration field never expire. + * + * @param sleCredential The credential SLE to inspect. + * @param closed The parent ledger's close time. Must be a + * NetClock epoch value — do not pass wall-clock time. + * @return `true` if the credential has expired, `false` otherwise. + */ bool checkExpired(SLE const& sleCredential, NetClock::time_point const& closed); -// Actually remove a credentials object from the ledger +/** Remove a credential SLE and its entries from both owner directories. + * + * A credential is indexed in two owner directories — the issuer's and the + * subject's. Reserve-count accounting depends on acceptance state: + * - Before acceptance (`lsfAccepted` unset): only the issuer holds the + * reserve; only the issuer's count is decremented. + * - After acceptance with distinct accounts: the subject holds the reserve + * and its count is decremented. + * - When issuer and subject are the same account, only one directory + * removal is performed. + * + * @note Paths indicating ledger corruption (missing account SLE, failed + * `dirRemove`) are marked `LCOV_EXCL` and are unreachable under normal + * operation. + * + * @param view Mutable ledger view through which the SLE is erased. + * @param sleCredential The credential SLE to delete; must not be null. + * @param j Journal for fatal-level error logging. + * @return `tesSUCCESS` on success; `tecNO_ENTRY` if @p sleCredential is + * null; `tecINTERNAL` or `tefBAD_LEDGER` on internal directory + * inconsistency. + */ [[nodiscard]] TER deleteSLE(ApplyView& view, std::shared_ptr const& sleCredential, beast::Journal j); -// Amendment and parameters checks for sfCredentialIDs field +/** Validate the `sfCredentialIDs` field of a transaction at preflight time. + * + * Enforces non-empty, at most `kMAX_CREDENTIALS_ARRAY_SIZE` entries, and no + * duplicate hashes. Returns `tesSUCCESS` immediately when `sfCredentialIDs` + * is absent, as credentials are optional for most transaction types. + * + * @param tx The transaction under preflight validation. + * @param j Journal for trace-level malformed-transaction logging. + * @return `tesSUCCESS` if the field is absent or valid; `temMALFORMED` if + * the array is empty, too large, or contains duplicates. + */ NotTEC checkFields(STTx const& tx, beast::Journal j); -// Accessing the ledger to check if provided credentials are valid. Do not use -// in doApply (only in preclaim) since it does not remove expired credentials. -// If you call it in preclaim, you also must call verifyDepositPreauth in -// doApply +/** Verify that all credentials in a transaction exist, are owned by the + * sender, and have been accepted — for use in preclaim only. + * + * Checks each ID in `sfCredentialIDs`: the SLE must exist, its `sfSubject` + * must equal @p src, and `lsfAccepted` must be set. Expiration is + * deliberately not checked here; expired credentials are deleted in doApply + * by `verifyDepositPreauth` or `verifyValidDomain`. + * + * @note If this returns `tesSUCCESS` in preclaim, the caller must invoke + * `verifyDepositPreauth` in doApply to garbage-collect any credentials + * that expire before the enclosing transaction applies. + * + * @param tx The transaction whose `sfCredentialIDs` field is inspected. + * @param view Read-only ledger view for SLE lookups. + * @param src The account that must own every listed credential. + * @param j Journal for trace-level logging. + * @return `tesSUCCESS` if `sfCredentialIDs` is absent or all credentials are + * valid; `tecBAD_CREDENTIALS` if any credential is missing, belongs to a + * different account, or has not been accepted. + */ TER valid(STTx const& tx, ReadView const& view, AccountID const& src, beast::Journal j); -// Check if subject has any credential maching the given domain. If you call it -// in preclaim and it returns tecEXPIRED, you should call verifyValidDomain in -// doApply. This will ensure that expired credentials are deleted. +/** Check whether @p subject holds a live, accepted credential for a + * permissioned domain — for use in preclaim only. + * + * Reads the `PermissionedDomain` SLE, iterates its `sfAcceptedCredentials` + * array, and looks up the corresponding credential SLE for @p subject. + * A credential qualifies when it exists, has not expired, and carries + * `lsfAccepted`. + * + * Because a `ReadView` is immutable, expired credentials cannot be deleted + * here. The function returns `tecEXPIRED` when all matching credentials + * are expired — signaling the caller that the condition may resolve in + * doApply where `verifyValidDomain` will physically remove them. + * + * @note If this returns `tecEXPIRED` in preclaim, the caller must invoke + * `verifyValidDomain` in doApply so that expired objects are + * garbage-collected even if the transaction ultimately fails. + * + * @param view Read-only ledger view. + * @param domainID Key of the `PermissionedDomain` SLE to check against. + * @param subject Account that must hold a qualifying credential. + * @return `tesSUCCESS` if a live accepted credential exists; `tecEXPIRED` + * if only expired credentials were found; `tecNO_AUTH` if no matching + * credential exists; `tecOBJECT_NOT_FOUND` if the domain does not exist. + */ TER validDomain(ReadView const& view, uint256 domainID, AccountID const& subject); -// This function is only called when we about to return tecNO_PERMISSION -// because all the checks for the DepositPreauth authorization failed. +/** Check whether a set of credential IDs matches a credential-set + * `DepositPreauth` entry for the destination account. + * + * Builds a sorted `std::set>` of + * `(issuer, credentialType)` pairs from @p credIDs and tests for the + * existence of the corresponding `keylet::depositPreauth(dst, sorted)`. + * The sorted representation matches the canonical key used at + * `DepositPreauth` creation time. + * + * @note Credential existence is assumed to have been confirmed in preclaim. + * A missing SLE here indicates an internal consistency error. + * @note `Slice` members in the internal sorted set are non-owning views + * into SLE storage. A `lifeExtender` vector keeps the SLEs alive for + * the duration of the lookup. + * + * @param view Read-only ledger view for SLE and keylet lookups. + * @param credIDs The `sfCredentialIDs` vector from the transaction. + * @param dst The destination account whose `DepositPreauth` is checked. + * @return `tesSUCCESS` if a matching `DepositPreauth` object exists; + * `tecNO_PERMISSION` if none exists; `tefINTERNAL` if a credential SLE + * is unexpectedly missing or a duplicate pair is encountered. + */ TER authorizedDepositPreauth(ReadView const& view, STVector256 const& ctx, AccountID const& dst); -// Sort credentials array, return empty set if there are duplicates +/** Build a sorted `(issuer, credentialType)` set from a credentials array. + * + * Produces the canonical representation used to key `DepositPreauth` + * objects. Each element of @p credentials must carry `sfIssuer` and + * `sfCredentialType`. + * + * @param credentials An `STArray` of credential pairs, as stored in a + * `DepositPreauth` or `PermissionedDomainSet` transaction. + * @return A sorted set of `(AccountID, Slice)` pairs; an empty set if any + * duplicate `(issuer, credentialType)` pair is detected. + */ std::set> makeSorted(STArray const& credentials); -// Check credentials array passed to DepositPreauth/PermissionedDomainSet -// transactions +/** Validate a credential array in `DepositPreauth` or + * `PermissionedDomainSet` transactions at preflight time. + * + * Credentials in these transactions are `(issuer, credentialType)` pairs + * rather than object hashes. Enforces: non-empty; at most @p maxSize + * entries; valid issuer `AccountID`; `sfCredentialType` length in + * `[1, kMAX_CREDENTIAL_TYPE_LENGTH]` bytes; and no logical duplicates + * (detected via `sha512Half(issuer, credentialType)`). + * + * @param credentials The `STArray` of credential pairs to validate. + * @param maxSize Maximum permitted array length (caller-supplied per + * transaction type). + * @param j Journal for trace-level malformed-transaction logging. + * @return `tesSUCCESS` if all entries are valid; `temARRAY_EMPTY`, + * `temARRAY_TOO_LARGE`, `temINVALID_ACCOUNT_ID`, or `temMALFORMED` + * on the first constraint violation found. + */ NotTEC checkArray(STArray const& credentials, unsigned maxSize, beast::Journal j); } // namespace credentials -// Check expired credentials and for credentials maching DomainID of the ledger -// object +/** Enforce domain-credential authorization in doApply, deleting expired + * credentials as a side effect. + * + * The doApply counterpart to `credentials::validDomain`. Collects all + * credential SLEs for @p account that match the `sfAcceptedCredentials` + * list of the `PermissionedDomain` at @p domainID, calls + * `credentials::removeExpired` to physically delete any that have expired, + * then re-checks whether at least one live, accepted credential remains. + * + * The two-pass design (collect → expire → re-validate) ensures expired + * objects are garbage-collected even when the surrounding transaction + * ultimately fails. + * + * @param view Mutable ledger view; expired credential SLEs are erased. + * @param account Account whose credentials are being verified. + * @param domainID Key of the `PermissionedDomain` SLE. + * @param j Journal for trace/error logging. + * @return `tesSUCCESS` if a live accepted credential for the domain exists; + * `tecEXPIRED` if only expired credentials were found; `tecNO_PERMISSION` + * if no matching credential exists; `tecOBJECT_NOT_FOUND` if the domain + * SLE is missing; or a propagated `TER` error from `removeExpired` under + * `fixCleanup3_1_3`. + */ TER verifyValidDomain(ApplyView& view, AccountID const& account, uint256 domainID, beast::Journal j); -// Check expired credentials and for existing DepositPreauth ledger object +/** Enforce deposit pre-authorization in doApply, deleting expired credentials + * as a side effect. + * + * Called by Payment, EscrowFinish, and PaymentChannelClaim when the + * destination account has `lsfDepositAuth` set. Authorization succeeds + * when any of the following hold: + * - `src == dst` (self-payments are always allowed). + * - `keylet::depositPreauth(dst, src)` exists (account-level pre-auth). + * - A credential-set `DepositPreauth` object exists for the credentials + * submitted via `sfCredentialIDs` (via `credentials::authorizedDepositPreauth`). + * + * If `sfCredentialIDs` is present, `credentials::removeExpired` is called + * unconditionally before the authorization tests. If any credential was + * expired, `tecEXPIRED` is returned immediately without attempting + * authorization. + * + * @param tx The transaction under doApply; may carry `sfCredentialIDs`. + * @param view Mutable ledger view; expired credential SLEs may be erased. + * @param src The sending account. + * @param dst The destination account. + * @param sleDst The destination account's SLE, used to test `lsfDepositAuth`. + * If null, `lsfDepositAuth` is treated as unset and the function returns + * `tesSUCCESS`. + * @param j Journal for trace/error logging. + * @return `tesSUCCESS` if authorized or `lsfDepositAuth` is not set; + * `tecEXPIRED` if submitted credentials have expired; + * `tecNO_PERMISSION` if no matching pre-authorization exists; or a + * propagated error from `removeExpired` or `authorizedDepositPreauth`. + */ TER verifyDepositPreauth( STTx const& tx, diff --git a/include/xrpl/ledger/helpers/DelegateHelpers.h b/include/xrpl/ledger/helpers/DelegateHelpers.h index 78ccc46d0b..99f56bd00a 100644 --- a/include/xrpl/ledger/helpers/DelegateHelpers.h +++ b/include/xrpl/ledger/helpers/DelegateHelpers.h @@ -1,3 +1,12 @@ +/** @file + * Runtime enforcement helpers for the XRPL delegate account system. + * + * Transactors call these two functions in sequence during permission + * validation: `checkTxPermission` for the broad transaction-type gate, + * then `loadGranularPermission` when a more restrictive, field-level + * check is needed. The permission schema and encoding convention live + * in `xrpl/protocol/Permissions.h`. + */ #pragma once #include @@ -7,24 +16,64 @@ namespace xrpl { -/** - * Check if the delegate account has permission to execute the transaction. - * @param delegate The delegate account. - * @param tx The transaction that the delegate account intends to execute. - * @return tesSUCCESS if the transaction is allowed, terNO_DELEGATE_PERMISSION - * if not. +/** Determine whether a delegate relationship grants blanket permission for + * a transaction type. + * + * Scans the `sfPermissions` array of the `ltDELEGATE` ledger entry for an + * element whose `sfPermissionValue` equals `tx.getTxnType() + 1` — the + * transaction-level encoding used on-ledger. Returns `tesSUCCESS` on the + * first match, or `terNO_DELEGATE_PERMISSION` if no match is found. + * + * A null `delegate` pointer is treated as a missing ledger entry and + * returns `terNO_DELEGATE_PERMISSION` immediately. + * + * The result is `NotTEC` (no `tec` fee-claim codes) because the two + * meaningful outcomes are `tesSUCCESS` and `terNO_DELEGATE_PERMISSION`. + * The `ter` (retry) code is intentional: the `ltDELEGATE` object could be + * updated in a subsequent ledger, so an identical transaction may succeed + * in the future without modification. + * + * @param delegate Immutable `ltDELEGATE` SLE obtained via `view.read()`; + * may be null, in which case `terNO_DELEGATE_PERMISSION` is returned. + * @param tx The transaction whose type is being checked. + * @return `tesSUCCESS` if the delegate holds a transaction-level permission + * for `tx`'s type; `terNO_DELEGATE_PERMISSION` otherwise. + * @note Callers should resolve the SLE via `keylet::delegate(account, + * delegate)` and pass it directly. If the SLE is absent from the + * ledger, `view.read()` returns null and the guard here handles it. + * @see loadGranularPermission — for fine-grained per-flag enforcement when + * this function returns `terNO_DELEGATE_PERMISSION`. */ NotTEC checkTxPermission(std::shared_ptr const& delegate, STTx const& tx); -/** - * Load the granular permissions granted to the delegate account for the - * specified transaction type - * @param delegate The delegate account. - * @param type Used to determine which granted granular permissions to load, - * based on the transaction type. - * @param granularPermissions Granted granular permissions tied to the - * transaction type. +/** Populate a set with all granular sub-operation permissions the delegate + * holds for a given transaction type. + * + * Walks the `sfPermissions` array of the `ltDELEGATE` ledger entry. For + * each element, it casts the `sfPermissionValue` to `GranularPermissionType` + * and asks `Permission::getInstance().getGranularTxType()` whether that + * granular type belongs to `type`. Matching values are inserted into + * `granularPermissions`. + * + * A null `delegate` pointer is a silent no-op; the output set is left + * unchanged. + * + * The set is caller-owned and passed by reference so transactors can declare + * it on the stack, avoiding heap allocation. Callers may also accumulate + * results from multiple calls if needed. + * + * @param delegate Immutable `ltDELEGATE` SLE; may be null (no-op). + * @param type The transaction type whose granular permissions should be + * collected (e.g., `ttTRUST_SET`, `ttPAYMENT`). + * @param granularPermissions Output set populated with every + * `GranularPermissionType` the delegate holds that maps to `type`. + * @note This function is the second stage of a two-step check. Call + * `checkTxPermission` first; only invoke this when that returns + * `terNO_DELEGATE_PERMISSION` and the transaction type supports + * granular flags. Calling it unconditionally wastes a full scan of + * the permissions array on the common case. + * @see checkTxPermission — for the broad transaction-type gate. */ void loadGranularPermission( diff --git a/include/xrpl/ledger/helpers/DirectoryHelpers.h b/include/xrpl/ledger/helpers/DirectoryHelpers.h index 2ae188182d..96f4d88e88 100644 --- a/include/xrpl/ledger/helpers/DirectoryHelpers.h +++ b/include/xrpl/ledger/helpers/DirectoryHelpers.h @@ -1,3 +1,22 @@ +/** @file + * Traversal utilities for ledger directory nodes (`ltDIR_NODE`). + * + * A directory is a linked list of pages (`SLE` of type `ltDIR_NODE`), + * where each page holds an `sfIndexes` field (`STVector256`) of child + * ledger-entry keys and an `sfIndexNext` field that chains to the next + * page. Owner directories track every object an account holds; order- + * book directories track standing offers at a given quality. + * + * This header provides: + * - A const-aware template core (`detail::internalDirFirst` / + * `detail::internalDirNext`) that unifies the read and write traversal + * paths at compile time. + * - A deprecated step-iterator API (`cdirFirst`, `cdirNext`, `dirFirst`, + * `dirNext`) used only where cursor patching during deletion is required. + * - Higher-level callback iterators (`forEachItem`, `forEachItemAfter`) + * for exhaustive and paginated walks. + * - `dirIsEmpty` and `describeOwnerDir` utility helpers. + */ #pragma once #include @@ -15,6 +34,32 @@ namespace xrpl { namespace detail { +/** Advance a directory cursor to the next entry, crossing page boundaries. + * + * When the cursor has consumed all entries in the current page, the function + * follows `sfIndexNext` to load the next page and tail-calls itself to yield + * the first entry of that page in a single logical step. If `sfIndexNext` is + * zero the directory is exhausted: `entry` is zeroed and `false` is returned. + * + * The `if constexpr` branch selects `view.read()` when `N` is `SLE const` + * (read-only traversal via `ReadView`) and `view.peek()` when `N` is `SLE` + * (mutable traversal via `ApplyView`), keeping both paths in one template. + * + * @tparam V A view type derived from `ReadView`. + * @tparam N Either `SLE` (mutable) or `SLE const` (read-only). + * @param view The ledger view to query pages from. + * @param root The 256-bit key of the directory's root (anchor) page. + * @param page In/out: the current page SLE; updated when a page boundary + * is crossed. + * @param index In/out: the zero-based cursor within `page->sfIndexes`; + * incremented to point past the entry that was just returned. + * @param entry Out: the key of the current entry on success; zeroed on + * end-of-directory. + * @return `true` if an entry was produced; `false` if the directory is + * exhausted. + * @note An `XRPL_ASSERT` fires in instrumented builds if `index` exceeds + * the page's entry count, indicating a corrupted cursor. + */ template < class V, class N, @@ -64,6 +109,23 @@ internalDirNext( return true; } +/** Initialise a directory cursor at the first entry of the root page. + * + * Loads the root page via `view.read()` (when `N` is `SLE const`) or + * `view.peek()` (when `N` is `SLE`), resets the index to zero, then + * delegates to `internalDirNext` to yield the first entry. + * + * @tparam V A view type derived from `ReadView`. + * @tparam N Either `SLE` (mutable) or `SLE const` (read-only). + * @param view The ledger view to query pages from. + * @param root The 256-bit key of the directory's root (anchor) page. + * @param page Out: set to the root page SLE on success; unchanged if the + * root page is absent. + * @param index Out: set to zero before delegating to `internalDirNext`. + * @param entry Out: the key of the first entry on success. + * @return `true` if the directory has at least one entry; `false` if the + * root page is absent or the directory is empty. + */ template < class V, class N, @@ -119,6 +181,24 @@ cdirFirst( unsigned int& index, uint256& entry); +/** Returns the first entry in the directory, advancing the index. + * + * Mutable overload of `cdirFirst` for use with `ApplyView`. Yields a + * `shared_ptr` obtained via `view.peek()`, allowing the caller to + * modify the page SLE if required. + * + * @deprecated Prefer the `Dir` range adaptor or `forEachItem` for new + * code. Use this overload only when cursor patching during deletion + * is required (see `cleanupOnAccountDelete` in `View.cpp`). + * + * @param view The mutable view against which to operate. + * @param root The 256-bit key of the directory's root page. + * @param page Out: set to the root page SLE obtained via `peek()`. + * @param index Out: set to the cursor position within `page->sfIndexes`. + * @param entry Out: the key of the first directory entry. + * @return `true` if the directory has at least one entry; `false` + * otherwise. + */ bool dirFirst( ApplyView& view, @@ -151,6 +231,31 @@ cdirNext( unsigned int& index, uint256& entry); +/** Advances the mutable directory cursor to the next entry. + * + * Mutable overload of `cdirNext` for use with `ApplyView`. Page + * transitions are handled transparently: when `index` reaches the end + * of the current page, `sfIndexNext` is followed and the cursor is reset + * to the first entry of the new page. + * + * @deprecated Prefer the `Dir` range adaptor or `forEachItem` for new + * code. The primary use case for this function is cursor patching + * during deletion: `cleanupOnAccountDelete` (in `View.cpp`) decrements + * `index` after each deletion so the cursor stays aligned as entries + * shift — a technique that relies on the cursor being externally + * accessible. + * + * @param view The mutable view against which to operate. + * @param root The 256-bit key of the directory's root page. + * @param page In/out: the current page SLE; updated on page boundary + * crossing. + * @param index In/out: the cursor position within `page->sfIndexes`; + * incremented past the returned entry. + * @param entry Out: the key of the current entry on success; zeroed when + * the directory is exhausted. + * @return `true` if an entry was produced; `false` if the directory is + * exhausted. + */ bool dirNext( ApplyView& view, @@ -160,19 +265,61 @@ dirNext( uint256& entry); /** @} */ -/** Iterate all items in the given directory. */ +/** Exhaustively walk every entry in a directory, invoking a callback for each. + * + * Iterates all pages of the directory in `sfIndexNext` chain order, calling + * `f` with the materialised child SLE for every key in `sfIndexes`. The + * child SLE is obtained via `view.read(keylet::child(key))` and may be + * `nullptr` if the referenced entry is absent from the view; the callback + * must handle that case. Iteration terminates when `sfIndexNext` is zero or + * a page SLE is missing; there is no early-exit mechanism. + * + * @param view The read-only ledger view to query. + * @param root Keylet of the directory's root page; must have type + * `ltDIR_NODE`. + * @param f Callback invoked with each child SLE (possibly `nullptr`). + * @note An `XRPL_ASSERT` fires in instrumented builds if `root.type` is + * not `ltDIR_NODE`; in release builds the function returns silently. + */ void forEachItem( ReadView const& view, Keylet const& root, std::function const&)> const& f); -/** Iterate all items after an item in the given directory. - @param after The key of the item to start after - @param hint The directory page containing `after` - @param limit The maximum number of items to return - @return `false` if the iteration failed -*/ +/** Paginated directory walk, delivering items that follow a cursor key. + * + * Supports cursor-based pagination as used by RPC handlers such as + * `account_offers`, `account_lines`, and `account_channels`. When + * `after` is non-zero the function first attempts to jump to the `hint` + * page (the page the client last saw) to avoid re-scanning all prior + * pages; if the hint does not contain `after`, it falls back to a linear + * scan from the root. Once the cursor is located, subsequent entries are + * delivered to `f` until `limit` is reached or the directory is exhausted. + * + * The callback `f` returns `bool`: `true` to continue (and decrement the + * limit counter), `false` to stop immediately regardless of the remaining + * limit. Callers conventionally request `limit + 1` items and infer a + * non-empty next page when exactly `limit + 1` items are delivered. + * + * @param view The read-only ledger view to query. + * @param root Keylet of the directory's root page; must have type + * `ltDIR_NODE`. + * @param after Cursor key: only entries that follow this key in directory + * order are delivered. Pass `uint256()` (zero) to start from the + * beginning, in which case the function always returns `true`. + * @param hint Page number expected to contain `after`; used as a fast- + * path optimisation. Ignored when `after` is zero or when the hint + * page does not actually contain `after`. + * @param limit Maximum number of `true`-returning callback invocations + * before the walk stops. + * @param f Callback invoked for each qualifying child SLE (possibly + * `nullptr` if the key is absent). Return `true` to continue + * iteration; `false` to stop early. + * @return `true` if `after` was found (or `after` is zero); `false` if + * the cursor key was never located, indicating a stale or invalid + * marker that callers should surface as a pagination error. + */ bool forEachItemAfter( ReadView const& view, @@ -182,7 +329,15 @@ forEachItemAfter( unsigned int limit, std::function const&)> const& f); -/** Iterate all items in an account's owner directory. */ +/** Exhaustively walk every entry in an account's owner directory. + * + * Convenience overload that resolves `id` to `keylet::ownerDir(id)` and + * forwards to `forEachItem(view, Keylet, f)`. + * + * @param view The read-only ledger view to query. + * @param id The account whose owner directory should be iterated. + * @param f Callback invoked with each child SLE (possibly `nullptr`). + */ inline void forEachItem( ReadView const& view, @@ -192,12 +347,22 @@ forEachItem( forEachItem(view, keylet::ownerDir(id), f); } -/** Iterate all items after an item in an owner directory. - @param after The key of the item to start after - @param hint The directory page containing `after` - @param limit The maximum number of items to return - @return `false` if the iteration failed -*/ +/** Paginated walk of an account's owner directory after a cursor key. + * + * Convenience overload that resolves `id` to `keylet::ownerDir(id)` and + * forwards to `forEachItemAfter(view, Keylet, after, hint, limit, f)`. + * + * @param view The read-only ledger view to query. + * @param id The account whose owner directory should be iterated. + * @param after Cursor key; pass `uint256()` (zero) to start from the + * beginning. + * @param hint Page number expected to contain `after`. + * @param limit Maximum number of `true`-returning callback invocations. + * @param f Callback invoked for each qualifying child SLE. Return `true` + * to continue; `false` to stop early. + * @return `true` if `after` was found (or is zero); `false` if the cursor + * was never located. + */ inline bool forEachItemAfter( ReadView const& view, @@ -210,13 +375,36 @@ forEachItemAfter( return forEachItemAfter(view, keylet::ownerDir(id), after, hint, limit, f); } -/** Returns `true` if the directory is empty - @param key The key of the directory -*/ +/** Returns `true` if the directory contains no entries. + * + * An empty `sfIndexes` array on the root page is necessary but not + * sufficient: the root is an anchor page and may have an empty index + * while `sfIndexNext` still points to a populated subsequent page. Both + * conditions — empty `sfIndexes` *and* `sfIndexNext == 0` — must hold + * before declaring the directory empty. A missing root SLE is also + * treated as empty. + * + * @param view The read-only ledger view to query. + * @param k Keylet of the directory's root page. + * @return `true` if the directory has no entries or does not exist; + * `false` otherwise. + */ [[nodiscard]] bool dirIsEmpty(ReadView const& view, Keylet const& k); -/** Returns a function that sets the owner on a directory SLE */ +/** Returns a callback that stamps a new directory page with its owner account. + * + * The returned `std::function` sets `sfOwner = account` on + * the newly allocated `ltDIR_NODE` SLE. It is passed as the `describe` + * argument to `ApplyView::dirInsert` throughout the codebase (e.g., + * `RippleStateHelpers.cpp`, `PaymentChannelCreate.cpp`) and is invoked only + * when `dirInsert` actually allocates a fresh overflow page, keeping the + * owning account ID out of the generic insertion logic. + * + * @param account The `AccountID` to record as `sfOwner` on each new page. + * @return A callable suitable for the `describe` parameter of + * `ApplyView::dirInsert`. + */ [[nodiscard]] std::function describeOwnerDir(AccountID const& account); diff --git a/include/xrpl/ledger/helpers/EscrowHelpers.h b/include/xrpl/ledger/helpers/EscrowHelpers.h index 5aa5214b1f..3d929a7aed 100644 --- a/include/xrpl/ledger/helpers/EscrowHelpers.h +++ b/include/xrpl/ledger/helpers/EscrowHelpers.h @@ -1,3 +1,13 @@ +/** @file + * Token-delivery helper for IOU and MPT escrow resolution. + * + * Implements `escrowUnlockApplyHelper`, the single function responsible for + * crediting the appropriate account when an IOU or MPT escrow is finished + * (`EscrowFinish`) or cancelled (`EscrowCancel`) under `featureTokenEscrow`. + * The function is specialised once for `Issue` (IOU trust-line path) and once + * for `MPTIssue` (MPToken path); callers reach the correct specialisation via + * `std::visit` on the `Asset` variant, with zero runtime dispatch overhead. + */ #pragma once #include @@ -13,6 +23,33 @@ namespace xrpl { +/** Credit an account with tokens held in escrow, applying transfer-fee logic. + * + * Primary template — no body is provided. Only the `Issue` and `MPTIssue` + * full specialisations are defined. Callers should invoke via `std::visit` + * on an `Asset` variant so the compiler selects the correct specialisation + * at compile time. + * + * @tparam T Asset type; must satisfy `ValidIssueType` (`Issue` or `MPTIssue`). + * @param view Mutable ledger view on which state changes are applied. + * @param lockedRate Transfer rate snapshotted at escrow creation time. + * Pass `kPARITY_RATE` for cancellations (return to sender, no fee). + * @param sleDest SLE for the destination account (`receiver`); used for + * owner-count and reserve checks when auto-creating a trust line or + * MPToken holding object. + * @param xrpBalance Pre-fee XRP balance of the destination account; compared + * against the incremental reserve required to create a new holding object. + * @param amount Escrowed token amount (face value locked at escrow creation). + * @param issuer Token issuer. + * @param sender Escrow creator / original token sender. + * @param receiver Account that will receive the unlocked tokens. + * @param createAsset When `true`, auto-creates a trust line or MPToken object + * for `receiver` if one does not already exist. Callers set this only + * when the transaction submitter is also the beneficiary, preserving + * account sovereignty over directory entries. + * @param journal Logging sink. + * @return `tesSUCCESS` on success, or a `tec` error code on failure. + */ template TER escrowUnlockApplyHelper( @@ -27,6 +64,40 @@ escrowUnlockApplyHelper( bool createAsset, beast::Journal journal); +/** IOU trust-line specialisation of `escrowUnlockApplyHelper`. + * + * Delivers IOU tokens from a finished or cancelled escrow to `receiver`, + * optionally creating the trust line and applying the snapshotted transfer + * fee. + * + * **Issuer short-circuits.** `sender == issuer` returns `tecINTERNAL` (an + * issuer cannot be an escrow originator for their own obligation). + * `receiver == issuer` returns `tesSUCCESS` immediately — delivery to the + * issuer is a redemption handled by the calling transactor at the balance + * level. + * + * **Trust line creation.** When `createAsset` is `true` and no trust line + * exists, one is created with a zero balance and zero limit via `trustCreate`. + * The `sfDefaultRipple` flag is inherited from `sleDest`. Reserve is checked + * first; insufficient reserve returns `tecNO_LINE_INSUF_RESERVE`. When + * `createAsset` is `false` and no line exists, returns `tecNO_LINE`. + * + * **Transfer fee.** The effective rate is `min(lockedRate, currentRate)`, + * protecting the receiver from a rate increase during the escrow lifetime. + * The fee is deducted *from* `amount` (not added on top), so `receiver` gets + * `amount - fee`. When neither party is the issuer and the rate differs from + * `kPARITY_RATE`, the check against the trust-line limit uses `finalAmt`. + * + * **Limit check.** When `createAsset` is `false`, the post-transfer balance + * is compared to `receiver`'s trust-line limit; `tecLIMIT_EXCEEDED` is + * returned if the delivery would exceed it. This check is skipped when + * `createAsset` is `true` because a freshly created line has a zero limit + * and would always fail it spuriously. + * + * @note This function is reached via `std::visit` on an `Asset` variant in + * `EscrowFinish` and `EscrowCancel`. `EscrowCancel` always passes + * `kPARITY_RATE` so no fee is charged on the return-to-sender path. + */ template <> inline TER escrowUnlockApplyHelper( @@ -70,21 +141,21 @@ escrowUnlockApplyHelper( initialBalance.get().account = noAccount(); if (TER const ter = trustCreate( - view, // payment sandbox - recvLow, // is dest low? - issuer, // source - receiver, // destination - trustLineKey.key, // ledger index - sleDest, // Account to add to - false, // authorize account - (sleDest->getFlags() & lsfDefaultRipple) == 0, // - false, // freeze trust line - false, // deep freeze trust line - initialBalance, // zero initial balance - Issue(currency, receiver), // limit of zero - 0, // quality in - 0, // quality out - journal); // journal + view, + recvLow, + issuer, + receiver, + trustLineKey.key, + sleDest, + false, + (sleDest->getFlags() & lsfDefaultRipple) == 0, + false, + false, + initialBalance, + Issue(currency, receiver), + 0, + 0, + journal); !isTesSuccess(ter)) { return ter; // LCOV_EXCL_LINE @@ -97,57 +168,43 @@ escrowUnlockApplyHelper( return tecNO_LINE; auto const xferRate = transferRate(view, amount); - // update if issuer rate is less than locked rate + // Cap to the lower of the snapshotted and current rate to protect the receiver. if (xferRate < lockedRate) lockedRate = xferRate; - // Transfer Rate only applies when: - // 1. Issuer is not involved in the transfer (senderIssuer or - // receiverIssuer) - // 2. The locked rate is different from the parity rate - - // NOTE: Transfer fee in escrow works a bit differently from a normal - // payment. In escrow, the fee is deducted from the locked/sending amount, - // whereas in a normal payment, the transfer fee is taken on top of the - // sending amount. + // Fee is deducted from `amount` (not added on top): finalAmt = amount - fee. + // No fee when either party is the issuer, or when lockedRate == kPARITY_RATE. auto finalAmt = amount; if ((!senderIssuer && !receiverIssuer) && lockedRate != kPARITY_RATE) { - // compute transfer fee, if any auto const xferFee = amount.value() - divideRound(amount, lockedRate, amount.get(), true); - // compute balance to transfer finalAmt = amount.value() - xferFee; } - // validate the line limit if the account submitting txn is not the receiver - // of the funds + // Limit check skipped when createAsset is true (freshly created line has + // zero limit and would always fail spuriously). if (!createAsset) { auto const sleRippleState = view.peek(trustLineKey); if (!sleRippleState) return tecINTERNAL; // LCOV_EXCL_LINE - // if the issuer is the high, then we use the low limit - // otherwise we use the high limit + // recvLow true → receiver is low side → use sfLowLimit; else sfHighLimit. STAmount const lineLimit = sleRippleState->getFieldAmount(recvLow ? sfLowLimit : sfHighLimit); STAmount lineBalance = sleRippleState->getFieldAmount(sfBalance); - // flip the sign of the line balance if the issuer is not high if (!recvLow) lineBalance.negate(); - // add the final amount to the line balance lineBalance += finalAmt; - // if the transfer would exceed the line limit return tecLIMIT_EXCEEDED if (lineLimit < lineBalance) return tecLIMIT_EXCEEDED; } - // if destination is not the issuer then transfer funds if (!receiverIssuer) { auto const ter = directSendNoFee(view, issuer, receiver, finalAmt, true, journal); @@ -157,6 +214,32 @@ escrowUnlockApplyHelper( return tesSUCCESS; } +/** MPT specialisation of `escrowUnlockApplyHelper`. + * + * Delivers MPT tokens from a finished or cancelled escrow to `receiver`, + * optionally creating an MPToken holding object and applying the snapshotted + * transfer fee. + * + * **MPToken creation.** When `createAsset` is `true`, `receiver` is not the + * issuer, and no MPToken SLE exists for this issuance, one is created via + * `createMPToken` and the owner count is incremented. Insufficient reserve + * returns `tecINSUFFICIENT_RESERVE`. If no MPToken exists after the creation + * attempt (and `receiver` is not the issuer), returns `tecNO_PERMISSION`. + * + * **Transfer fee.** Identical to the `Issue` path: effective rate is + * `min(lockedRate, currentRate)`, fee is deducted *from* `amount`, and no + * fee is applied when either party is the issuer or the rate is parity. + * + * **`fixTokenEscrowV1` bug fix.** The gross amount passed to `unlockEscrowMPT` + * (used to reduce `sfOutstandingAmount`) is `amount` when the amendment is + * enabled, and `finalAmt` otherwise. Without the fix, the outstanding supply + * is only reduced by the net delivered amount, silently retaining the fee + * portion; with the fix, the full face value is removed from circulation and + * the fee is burned from the outstanding supply. + * + * @note `EscrowCancel` passes `kPARITY_RATE` so no fee is charged when + * tokens are returned to the original sender. + */ template <> inline TER escrowUnlockApplyHelper( @@ -189,7 +272,6 @@ escrowUnlockApplyHelper( return ter; // LCOV_EXCL_LINE } - // update owner count. adjustOwnerCount(view, sleDest, 1, journal); } @@ -197,25 +279,16 @@ escrowUnlockApplyHelper( return tecNO_PERMISSION; auto const xferRate = transferRate(view, amount); - // update if issuer rate is less than locked rate + // Cap to the lower of the snapshotted and current rate to protect the receiver. if (xferRate < lockedRate) lockedRate = xferRate; - // Transfer Rate only applies when: - // 1. Issuer is not involved in the transfer (senderIssuer or - // receiverIssuer) - // 2. The locked rate is different from the parity rate - - // NOTE: Transfer fee in escrow works a bit differently from a normal - // payment. In escrow, the fee is deducted from the locked/sending amount, - // whereas in a normal payment, the transfer fee is taken on top of the - // sending amount. + // Fee is deducted from `amount` (not added on top): finalAmt = amount - fee. + // No fee when either party is the issuer, or when lockedRate == kPARITY_RATE. auto finalAmt = amount; if ((!senderIssuer && !receiverIssuer) && lockedRate != kPARITY_RATE) { - // compute transfer fee, if any auto const xferFee = amount.value() - divideRound(amount, lockedRate, amount.asset(), true); - // compute balance to transfer finalAmt = amount.value() - xferFee; } return unlockEscrowMPT( diff --git a/include/xrpl/ledger/helpers/LendingHelpers.h b/include/xrpl/ledger/helpers/LendingHelpers.h index b9711c4053..ea997a2fd4 100644 --- a/include/xrpl/ledger/helpers/LendingHelpers.h +++ b/include/xrpl/ledger/helpers/LendingHelpers.h @@ -1,109 +1,167 @@ #pragma once +/** @file + * Computational core of the XLS-66 lending protocol. + * + * Defines the data structures and mathematical primitives that model every + * stage of an amortized loan's life cycle: computing periodic payments, + * splitting each payment into principal / interest / management-fee + * components, and handling regular, late, full (early-closure), and + * overpayment scenarios. The top-level entry point `loanMakePayment()` + * implements `make_payment` from XLS-66 §3.2.4.4. Every lending transactor + * (`LoanSet`, `LoanPay`, `LoanDelete`, `LoanBroker*`) either calls these + * functions or operates on the structures defined here. + */ + #include #include #include namespace xrpl { -// Lending protocol has dependencies, so capture them here. +/** Verify that all amendment prerequisites for the lending protocol are active. + * + * Every lending transactor calls this in `checkExtraFeatures()`. Adding a new + * prerequisite here gates all lending transactions atomically, so callers do + * not need to replicate the dependency list. + * + * @param rules Active amendment rules for the current ledger. + * @param tx The transaction being validated; inspected for optional fields + * (e.g., `sfDomainID`) that require additional amendments. + * @return `true` if all required amendments are enabled and the transaction is + * consistent with them; `false` if the caller must reject the transaction. + */ bool checkLendingProtocolDependencies(Rules const& rules, STTx const& tx); +/** Number of seconds in a 365-day year; used to prorate annual rates. */ static constexpr std::uint32_t kSECONDS_IN_YEAR = 365 * 24 * 60 * 60; +/** Convert an annualized interest rate to a per-payment-period rate. + * + * Prorates the annual rate by the fraction `paymentInterval / kSECONDS_IN_YEAR`. + * Implements Equation (1) from XLS-66, Section A-2 Equation Glossary. All + * amortization math in this module flows from this single conversion. + * + * @param interestRate Annual interest rate in tenth-of-a-basis-point units. + * @param paymentInterval Length of one payment period in seconds. + * @return The per-period rate as a `Number` at full floating-point precision. + */ Number loanPeriodicRate(TenthBips32 interestRate, std::uint32_t paymentInterval); -/// Ensure the periodic payment is always rounded consistently +/** Round a periodic payment amount upward to the asset's representable precision. + * + * Borrowers must never benefit from rounding, so periodic payments are always + * rounded upward. Delegates to `roundToAsset(..., Number::RoundingMode::Upward)`. + * + * @param asset Asset whose representable precision constrains rounding. + * @param periodicPayment Unrounded installment amount. + * @param scale Exponent that defines the target precision. + * @return The smallest representable value >= `periodicPayment` at `scale`. + */ inline Number roundPeriodicPayment(Asset const& asset, Number const& periodicPayment, std::int32_t scale) { return roundToAsset(asset, periodicPayment, scale, Number::RoundingMode::Upward); } -/* Represents the breakdown of amounts to be paid and changes applied to the - * Loan object while processing a loan payment. +/** Breakdown of amounts paid and loan-object changes produced by one payment. * - * This structure is returned after processing a loan payment transaction and - * captures the amounts that need to be paid. The actual ledger entry changes - * are made in LoanPay based on this structure values. + * Returned by `loanMakePayment()` after a payment is processed. The three + * non-negative fields (`principalPaid`, `interestPaid`, `feePaid`) are the + * amounts disbursed to the loan principal, the vault, and the broker + * respectively. `valueChange` records unexpected movement in the total + * outstanding balance: * - * The sum of principalPaid, interestPaid, and feePaid represents the total - * amount to be deducted from the borrower's account. The valueChange field - * tracks whether the loan's total value increased or decreased beyond normal - * amortization. + * - **0** for a well-timed regular payment. + * - **negative** for an overpayment (extra principal reduces future interest). + * - **positive** for a late payment (penalty interest increased the debt). * - * This structure is explained in the XLS-66 spec, section 3.2.4.2 (Payment - * Processing). + * `operator+=` accumulates results across multiple payment rounds within a + * single transaction. + * + * @note Defined in XLS-66 §3.2.4.2 (Payment Processing). */ struct LoanPaymentParts { - // The amount of principal paid that reduces the loan balance. - // This amount is subtracted from sfPrincipalOutstanding in the Loan object - // and paid to the Vault + /** Amount that reduces `sfPrincipalOutstanding`; paid to the vault. */ Number principalPaid = kNUM_ZERO; - // The total amount of interest paid to the Vault. - // This includes: - // - Tracked interest from the amortization schedule - // - Untracked interest (e.g., late payment penalty interest) - // This value is always non-negative. + /** Total interest paid to the vault, including both tracked amortization + * interest and any untracked late-payment penalty interest. Always >= 0. + */ Number interestPaid = kNUM_ZERO; - // The change in the loan's total value outstanding. - // - If valueChange < 0: Loan value decreased - // - If valueChange > 0: Loan value increased - // - If valueChange = 0: No value adjustment - // - // For regular on-time payments, this is always 0. Non-zero values occur - // when: - // - Overpayments reduce the loan balance beyond the scheduled amount - // - Late payments add penalty interest to the loan value - // - Early full payment may increase or decrease the loan value based on - // terms + /** Net change in the loan's total outstanding value. + * + * Zero for on-time regular payments. Negative when an overpayment + * reduces the principal beyond the scheduled amount. Positive when a + * late-payment penalty interest increases the debt. + */ Number valueChange = kNUM_ZERO; - /* The total amount of fees paid to the Broker. - * This includes: - * - Tracked management fees from the amortization schedule - * - Untracked fees (e.g., late payment fees, service fees, origination - * fees) This value is always non-negative. + /** Total fees paid to the broker, including tracked management fees and any + * untracked late or service fees. Always >= 0. */ Number feePaid = kNUM_ZERO; + /** Accumulate payment parts from a subsequent payment round. + * + * Used by `loanMakePayment()` when a single transaction covers more than + * one installment. Asserts that all fields of `other` are non-negative. + * + * @param other Parts from the next completed round. + * @return Reference to `*this` with all fields incremented. + */ LoanPaymentParts& operator+=(LoanPaymentParts const& other); + /** Compare two `LoanPaymentParts` for exact equality across all four fields. + * + * @param other The instance to compare against. + * @return `true` if every field compares equal. + */ bool operator==(LoanPaymentParts const& other) const; }; -/** This structure captures the parts of a loan state. +/** Snapshot of the financial state of a loan at a point in time. * - * Whether the values are theoretical (unrounded) or rounded will depend on how - * it was computed. + * Holds the four numbers that describe where a loan stands. Values may be + * theoretical (unrounded, from `computeTheoreticalLoanState()`) or rounded to + * ledger precision (from `constructRoundedLoanState()`), depending on context. * - * Many of the fields can be derived from each other, but they're all provided - * here to reduce code duplication and possible mistakes. - * e.g. - * * interestOutstanding = valueOutstanding - principalOutstanding - * * interestDue = interestOutstanding - managementFeeDue + * @invariant `interestDue + managementFeeDue == valueOutstanding - principalOutstanding`. + * This is enforced at runtime by `XRPL_ASSERT_PARTS` inside `interestOutstanding()`. + * + * Many fields are derivable from each other; they are all stored explicitly to + * reduce duplication and the risk of sign/orientation mistakes. */ struct LoanState { - // Total value still due to be paid by the borrower. + /** Total value still owed by the borrower (`sfTotalValueOutstanding`). */ Number valueOutstanding; - // Principal still due to be paid by the borrower. + + /** Principal component still outstanding (`sfPrincipalOutstanding`). */ Number principalOutstanding; - // Interest still due to be paid to the Vault. - // This is a portion of interestOutstanding + + /** Net interest due to the vault; equals + * `valueOutstanding - principalOutstanding - managementFeeDue`. + */ Number interestDue; - // Management fee still due to be paid to the broker. - // This is a portion of interestOutstanding + + /** Management fee due to the broker (`sfManagementFeeOutstanding`); + * a sub-portion of the gross interest outstanding. + */ Number managementFeeDue; - // Interest still due to be paid by the borrower. + /** Sum of `interestDue` and `managementFeeDue`. + * + * Asserts the `LoanState` invariant before returning. + * + * @return `interestDue + managementFeeDue`, i.e., the gross interest still owed. + */ [[nodiscard]] Number interestOutstanding() const { @@ -115,42 +173,60 @@ struct LoanState } }; -/* Describes the initial computed properties of a loan. +/** Fully-initialized description of a loan's payment structure. * - * This structure contains the fundamental calculated values that define a - * loan's payment structure and amortization schedule. These properties are - * computed: - * - At loan creation (LoanSet transaction) - * - When loan terms change (e.g., after an overpayment that reduces the loan - * balance) + * Computed by `computeLoanProperties()` at loan creation (`LoanSet`) and + * after each overpayment re-amortization. Passed to `checkLoanGuards()` for + * validation before being written to the Loan ledger entry. + * + * The `loanScale` is derived dynamically from the `STAmount` exponent of the + * rounded total value outstanding and clamped to `minimumScale`. Using a + * consistent scale for all subsequent rounding prevents dust-accumulation bugs + * where tiny remainders can never be fully cleared. */ struct LoanProperties { - // The unrounded amount to be paid at each regular payment period. - // Calculated using the standard amortization formula based on principal, - // interest rate, and number of payments. - // The actual amount paid in the LoanPay transaction must be rounded up to - // the precision of the asset and loan. + /** Unrounded fixed installment amount computed from the amortization formula. + * + * The amount actually required from the borrower each period is this value + * rounded upward via `roundPeriodicPayment()`. + */ Number periodicPayment; - // The loan's current state, with all values rounded to the loan's scale. + /** Current loan state with all fields rounded to `loanScale`. */ LoanState loanState; - // The scale (decimal places) used for rounding all loan amounts. - // This is the maximum of: - // - The asset's native scale - // - A minimum scale required to represent the periodic payment accurately - // All loan state values (principal, interest, fees) are rounded to this - // scale. + /** Decimal exponent used for rounding all loan amounts. + * + * Set to the maximum of the asset's native scale and a minimum scale + * sufficient to represent the periodic payment accurately. All principal, + * interest, and fee values are rounded to this exponent. + */ std::int32_t loanScale{}; - // The principal portion of the first payment. + /** Unrounded principal portion of the very first periodic payment. + * + * Checked by `checkLoanGuards()` to ensure it is > 0: the first payment + * pays the least principal in an amortized schedule, so if it is positive + * then all subsequent payments will also reduce principal. + */ Number firstPaymentPrincipal; }; -// Some values get re-rounded to the vault scale any time they are adjusted. In -// addition, they are prevented from ever going below zero. This helps avoid -// accumulated rounding errors and leftover dust amounts. +/** Apply an adjustment to a loan value, re-round to vault scale, and clamp to zero. + * + * Certain loan values are re-rounded to the vault scale every time they are + * adjusted, preventing the accumulation of rounding dust across many payment + * cycles. If the result would be negative (possible due to sub-scale rounding + * errors), it is clamped to zero. + * + * @tparam NumberProxy A proxy type that supports assignment and dereferencing + * to `Number` (e.g., `STObject::ValueProxy`). + * @param value Proxy to the value being adjusted; updated in place. + * @param adjustment The signed delta to apply before re-rounding. + * @param asset Asset that constrains representable precision. + * @param vaultScale Exponent for re-rounding the result. + */ template void adjustImpreciseNumber( @@ -165,6 +241,16 @@ adjustImpreciseNumber( value = 0; } +/** Extract the decimal scale of a vault's `sfAssetsTotal` field. + * + * Returns `Number::kMIN_EXPONENT - 1` (an unusably small sentinel) when + * `vaultSle` is null, so callers can detect the invalid case without a + * separate null check. + * + * @param vaultSle Const reference to the Vault SLE; may be null. + * @return The scale of `sfAssetsTotal` relative to `sfAsset`, or a sentinel + * value if `vaultSle` is null. + */ inline int getAssetsTotalScale(SLE::const_ref vaultSle) { @@ -173,6 +259,30 @@ getAssetsTotalScale(SLE::const_ref vaultSle) return scale(vaultSle->at(sfAssetsTotal), vaultSle->at(sfAsset)); } +/** Validate that a set of computed loan properties is self-consistent and payable. + * + * Enforces four guards derived from XLS-66 to reject loans that would fail + * to amortize correctly under the spec's rounding rules: + * + * 1. If `expectInterest` is `true`, total lifetime interest must be > 0. + * 2. `firstPaymentPrincipal` must be > 0 (ensures every payment reduces principal). + * 3. The rounded periodic payment must not round to zero. + * 4. `floor(valueOutstanding / roundedPayment)` must equal `paymentTotal` + * (ensures the loan closes in exactly the specified number of installments). + * + * Called from loan creation (`LoanSet::doApply()`) and after each overpayment + * re-amortization inside `detail::tryOverpayment()`. + * + * @param vaultAsset Asset used for rounding the periodic payment. + * @param principalRequested Loan principal; used to derive total interest outstanding. + * @param expectInterest `true` when the loan's interest rate is non-zero. + * @param paymentTotal Total number of scheduled payments. + * @param properties Computed loan properties to validate. + * @param j Journal for diagnostic log messages. + * @return `tesSUCCESS` if all guards pass; `tecPRECISION_LOSS` when the loan + * cannot be amortized accurately at the given principal / rate / scale + * combination; `tecINTERNAL` for unexpected internal inconsistencies. + */ TER checkLoanGuards( Asset const& vaultAsset, @@ -182,6 +292,23 @@ checkLoanGuards( LoanProperties const& properties, beast::Journal j); +/** Compute the theoretically correct loan state at full arithmetic precision. + * + * Derives what each outstanding balance *should* be purely from the payment + * schedule, with no ledger-rounding effects. Used as a target in + * `computePaymentComponents()` and `detail::tryOverpayment()` to measure and + * correct accumulated rounding drift. Implements `calculate_true_loan_state` + * from XLS-66 §3.2.4.4 (Equations 30-33 from Section A-2). + * + * @param rules Active amendment rules (passed to the internal + * `loanPrincipalFromPeriodicPayment()` call). + * @param periodicPayment Fixed installment amount. + * @param periodicRate Pre-computed per-period interest rate. + * @param paymentRemaining Number of payments still remaining after this point. + * @param managementFeeRate Broker fee rate in tenth-bips. + * @return Unrounded `LoanState` derived from the schedule, or a fully-zeroed + * state if `paymentRemaining == 0`. + */ LoanState computeTheoreticalLoanState( Rules const& rules, @@ -190,18 +317,48 @@ computeTheoreticalLoanState( std::uint32_t const paymentRemaining, TenthBips32 const managementFeeRate); -// Constructs a valid LoanState object from arbitrary inputs +/** Build a `LoanState` from the three directly-tracked loan balances. + * + * Derives `interestDue = totalValueOutstanding - principalOutstanding - + * managementFeeOutstanding` rather than accepting it as a parameter, ensuring + * the `LoanState` invariant always holds. Prefer this over constructing + * `LoanState` directly to avoid sign/ordering mistakes. + * + * @param totalValueOutstanding Total value still owed by the borrower. + * @param principalOutstanding Principal component still outstanding. + * @param managementFeeOutstanding Management fee component still outstanding. + * @return `LoanState` with `interestDue` derived from the other three fields. + */ LoanState constructLoanState( Number const& totalValueOutstanding, Number const& principalOutstanding, Number const& managementFeeOutstanding); -// Constructs a valid LoanState object from a Loan object, which always has -// rounded values +/** Build a `LoanState` from a Loan ledger entry's current rounded values. + * + * Convenience wrapper that reads `sfTotalValueOutstanding`, + * `sfPrincipalOutstanding`, and `sfManagementFeeOutstanding` from the SLE + * and delegates to `constructLoanState()`. + * + * @param loan Const reference to the Loan SLE. + * @return `LoanState` reflecting the current rounded ledger values. + */ LoanState constructRoundedLoanState(SLE::const_ref loan); +/** Compute the broker's management fee on a given interest amount. + * + * Calculates `roundDown(tenthBipsOfValue(value, managementFeeRate), scale)`. + * Downward rounding ensures the vault always receives its full share. + * Implements Equation (32) from XLS-66, Section A-2 Equation Glossary. + * + * @param asset Asset used to constrain rounding. + * @param value Gross interest amount from which the fee is taken. + * @param managementFeeRate Broker's rate in tenth-of-a-basis-point units. + * @param scale Exponent for rounding the result downward. + * @return Broker fee rounded down to `scale`. + */ Number computeManagementFee( Asset const& asset, @@ -209,6 +366,25 @@ computeManagementFee( TenthBips32 managementFeeRate, std::int32_t scale); +/** Compute the total interest charge for an early full payment. + * + * Sums two components (Equations 27-28 from XLS-66, Section A-2): + * - Accrued interest since the last payment (`detail::loanAccruedInterest()`). + * - Prepayment penalty (`closeInterestRate` applied to the theoretical + * principal outstanding); zero when `closeInterestRate == 0`. + * + * @param theoreticalPrincipalOutstanding Unrounded principal derived from the + * payment schedule (not the rounded ledger value). + * @param periodicRate Pre-computed per-period interest rate. + * @param parentCloseTime Close time of the parent ledger (the "now" for the + * accrued-interest calculation). + * @param paymentInterval Payment period length in seconds. + * @param prevPaymentDate Due date of the most recently completed payment. + * @param startDate Loan start date. + * @param closeInterestRate Prepayment penalty rate in tenth-of-a-basis-point + * units; 0 means no prepayment penalty. + * @return `accruedInterest + prepaymentPenalty`, both non-negative. + */ Number computeFullPaymentInterest( Number const& theoreticalPrincipalOutstanding, @@ -223,94 +399,91 @@ namespace detail { // These classes and functions should only be accessed by LendingHelper // functions and unit tests -enum class PaymentSpecialCase { None, Final, Extra }; +/** Classification of a single payment's scheduling role. */ +enum class PaymentSpecialCase { + None, /**< Regular scheduled installment. */ + Final, /**< Last payment that closes out the loan. */ + Extra /**< Overpayment beyond the regular schedule. */ +}; -/* Represents a single loan payment component parts. - -* This structure captures the "delta" (change) values that will be applied to -* the tracked fields in the Loan ledger object when a payment is processed. -* -* These are called "deltas" because they represent the amount by which each -* corresponding field in the Loan object will be reduced. -* They are "tracked" as they change tracked loan values. -*/ +/** Tracked delta values that will be applied to the Loan ledger entry on payment. + * + * Each field represents the amount by which the corresponding `sf*` field in + * the Loan object will be reduced. These are "tracked" because they alter the + * loan's amortization schedule; contrast with the untracked amounts in + * `ExtendedPaymentComponents`. + * + * The relationship `trackedValueDelta == trackedPrincipalDelta + + * trackedInterestPart() + trackedManagementFeeDelta` must hold at all times. + */ struct PaymentComponents { - // The change in total value outstanding for this payment. - // This amount will be subtracted from sfTotalValueOutstanding in the Loan - // object. Equal to the sum of trackedPrincipalDelta, - // trackedInterestPart(), and trackedManagementFeeDelta. + /** Amount subtracted from `sfTotalValueOutstanding`. + * + * Equals `trackedPrincipalDelta + trackedInterestPart() + + * trackedManagementFeeDelta`. + */ Number trackedValueDelta; - // The change in principal outstanding for this payment. - // This amount will be subtracted from sfPrincipalOutstanding in the Loan - // object, representing the portion of the payment that reduces the - // original loan amount. + /** Amount subtracted from `sfPrincipalOutstanding`. */ Number trackedPrincipalDelta; - // The change in management fee outstanding for this payment. - // This amount will be subtracted from sfManagementFeeOutstanding in the - // Loan object. This represents only the tracked management fees from the - // amortization schedule and does not include additional untracked fees - // (such as late payment fees) that go directly to the broker. + /** Amount subtracted from `sfManagementFeeOutstanding`. + * + * Covers only scheduled management fees from the amortization table; + * unscheduled fees (e.g., late fees) live in `ExtendedPaymentComponents`. + */ Number trackedManagementFeeDelta; - // Indicates if this payment has special handling requirements. - // - none: Regular scheduled payment - // - final: The last payment that closes out the loan - // - extra: An additional payment beyond the regular schedule (overpayment) + /** Scheduling classification of this payment. */ PaymentSpecialCase specialCase = PaymentSpecialCase::None; - // Calculates the tracked interest portion of this payment. - // This is derived from the other components as: - // trackedValueDelta - trackedPrincipalDelta - trackedManagementFeeDelta - // - // @return The amount of tracked interest included in this payment that - // will be paid to the vault. + /** Net interest portion of this payment, paid to the vault. + * + * Derived as `trackedValueDelta - trackedPrincipalDelta - + * trackedManagementFeeDelta`. + * + * @return Scheduled interest component; always >= 0 for well-formed input. + */ [[nodiscard]] Number trackedInterestPart() const; }; -/* Extends PaymentComponents with untracked payment amounts. +/** `PaymentComponents` extended with untracked fees and interest. * - * This structure adds untracked fees and interest to the base - * PaymentComponents, representing amounts that don't affect the Loan object's - * tracked state but are still part of the total payment due from the borrower. + * Untracked amounts are paid out to the broker (`untrackedManagementFee`) and + * vault (`untrackedInterest`) without altering the Loan object's amortization + * state. They arise from charges outside the regular schedule — late payment + * penalties, service fees, origination fees. * - * Untracked amounts include: - * - Late payment fees that go directly to the Broker - * - Late payment penalty interest that goes directly to the Vault - * - Service fees + * `totalDue` is computed eagerly in the constructor as + * `trackedValueDelta + untrackedInterest + untrackedManagementFee`. * - * The key distinction is that tracked amounts reduce the Loan object's state - * (sfTotalValueOutstanding, sfPrincipalOutstanding, - * sfManagementFeeOutstanding), while untracked amounts are paid directly to the - * recipient without affecting the loan's amortization schedule. + * @note `untrackedManagementFee` and `untrackedInterest` may individually be + * negative (e.g., adjustments), but the corresponding fields in the + * returned `LoanPaymentParts` are always clamped to >= 0. */ struct ExtendedPaymentComponents : public PaymentComponents { - // Additional management fees that go directly to the Broker. - // This includes fees not part of the standard amortization schedule - // (e.g., late fees, service fees, origination fees). - // This value may be negative, though the final value returned in - // LoanPaymentParts.feePaid will never be negative. + /** Unscheduled fee paid directly to the broker (e.g., late fee, service fee). */ Number untrackedManagementFee; - // Additional interest that goes directly to the Vault. - // This includes interest not part of the standard amortization schedule - // (e.g., late payment penalty interest). - // This value may be negative, though the final value returned in - // LoanPaymentParts.interestPaid will never be negative. + /** Unscheduled interest paid directly to the vault (e.g., late penalty). */ Number untrackedInterest; - // The complete amount due from the borrower for this payment. - // Calculated as: trackedValueDelta + untrackedInterest + - // untrackedManagementFee - // - // This value is used to validate that the payment amount provided by the - // borrower is sufficient to cover all components of the payment. + /** Total amount due from the borrower for this payment. + * + * Equals `trackedValueDelta + untrackedInterest + untrackedManagementFee`. + * Used to verify the borrower's supplied amount is sufficient. + */ Number totalDue; + /** Construct from a base `PaymentComponents` plus the two untracked amounts. + * + * @param p Base tracked components. + * @param fee Untracked management fee for the broker. + * @param interest Untracked interest for the vault; defaults to zero. + */ ExtendedPaymentComponents(PaymentComponents const& p, Number fee, Number interest = kNUM_ZERO) : PaymentComponents(p) , untrackedManagementFee(fee) @@ -320,26 +493,27 @@ struct ExtendedPaymentComponents : public PaymentComponents } }; -/* Represents the differences between two loan states. +/** Component-wise difference between two `LoanState` objects. * - * This structure is used to capture the change in each component of a loan's - * state, typically when computing the difference between two LoanState objects - * (e.g., before and after a payment). It is a convenient way to capture changes - * in each component. How that difference is used depends on the context. + * Produced by `operator-(LoanState, LoanState)`. Used in `tryOverpayment()` + * to measure the gap between the theoretical (unrounded) loan state and the + * rounded ledger state, allowing accumulated rounding error to be preserved + * across re-amortization. */ struct LoanStateDeltas { - // The difference in principal outstanding between two loan states. + /** Change in principal outstanding. */ Number principal; - // The difference in interest due between two loan states. + /** Change in interest due. */ Number interest; - // The difference in management fee outstanding between two loan states. + /** Change in management fee outstanding. */ Number managementFee; - /* Calculates the total change across all components. - * @return The sum of principal, interest, and management fee deltas. + /** Sum of all three delta components. + * + * @return `principal + interest + managementFee`. */ [[nodiscard]] Number total() const @@ -347,11 +521,46 @@ struct LoanStateDeltas return principal + interest + managementFee; } - // Ensures all delta values are non-negative. + /** Clamp all fields to zero from below. + * + * Rounding can occasionally produce tiny negative deltas when the + * theoretical target exceeds the current rounded state by a sub-scale + * amount. This method eliminates those artifacts before the deltas are + * used as payment amounts. + */ void nonNegative(); }; +/** Simulate a principal overpayment and re-amortize the loan in a local sandbox. + * + * Cannot simply recalculate from scratch because accumulated rounding errors + * from the loan's history must be preserved. The algorithm: + * 1. Computes the theoretical (unrounded) current state from the schedule. + * 2. Measures the rounding error gap against the current ledger state. + * 3. Reduces the theoretical principal by the overpayment amount. + * 4. Calls `computeLoanProperties()` for the new schedule. + * 5. Re-applies the preserved rounding errors before final rounding. + * 6. Validates the result with `checkLoanGuards()`. + * + * Returns `Unexpected(tesSUCCESS)` (no error, but no commit) when the + * overpayment would leave the loan in an invalid state — the caller treats + * this as a silent no-op rather than a transaction failure. + * + * @param rules Active amendment rules. + * @param asset Loan asset. + * @param loanScale Current loan rounding exponent. + * @param overpaymentComponents Breakdown of the overpayment (tracked + untracked). + * @param roundedLoanState Current rounded loan state from the ledger. + * @param periodicPayment Current fixed installment amount. + * @param periodicRate Per-period interest rate. + * @param paymentRemaining Payments remaining before the overpayment. + * @param managementFeeRate Broker fee rate in tenth-bips. + * @param j Journal for diagnostic logging. + * @return Pair of (`LoanPaymentParts`, re-amortized `LoanProperties`) on success; + * `Unexpected(TER)` on a hard failure, or `Unexpected(tesSUCCESS)` when the + * overpayment is silently suppressed. + */ Expected, TER> tryOverpayment( Rules const& rules, @@ -365,18 +574,79 @@ tryOverpayment( TenthBips16 const managementFeeRate, beast::Journal j); +/** Compute `(1 + r)^n - 1` accurately for near-zero `r` via binomial expansion. + * + * Direct subtraction `power(1 + r, n) - 1` suffers catastrophic cancellation + * when `r` is small: the result `~r*n` sits far below the leading `1` in + * `(1+r)^n`, consuming most of `Number`'s 19-digit mantissa. The binomial + * expansion avoids this: + * + * `(1 + r)^n - 1 = nr + C(n,2) r^2 + ... + r^n` + * + * Each term is derived from the previous as + * `term_{k+1} = term_k * r * (n - k) / (k + 1)`. The loop terminates early + * when adding the next term leaves the running sum unchanged (below `Number`'s + * precision floor). + * + * @param periodicRate Per-period rate `r`; must be >= 0. + * @param paymentsRemaining Number of periods `n`. + * @return `(1 + r)^n - 1`, or 0 if `r == 0` or `n == 0`. + * @note For `r * n >= 1e-9` the closed-form path in `computePowerMinusOneHybrid()` + * is ~30-500x faster and equally accurate; prefer the hybrid for production use. + */ [[nodiscard]] Number computePowerMinusOne(Number const& periodicRate, std::uint32_t paymentsRemaining); +/** Compute `(1 + r)^n - 1`, selecting the numerically stable path automatically. + * + * When `r * n >= 1e-9` the closed-form `power(1 + r, n) - 1` retains enough + * precision and is ~30-500x faster than the binomial expansion. Below that + * threshold, catastrophic cancellation degrades the result to fewer than ~10 + * significant digits, so the call is forwarded to `computePowerMinusOne()`. + * + * @param periodicRate Per-period rate `r`; must be >= 0. + * @param paymentsRemaining Number of periods `n`. + * @return `(1 + r)^n - 1`, or 0 if `r == 0` or `n == 0`. + * @note The 1e-9 threshold is verified by `testComputePowerMinusOneHybrid` to + * yield agreement between both paths to within `Number`'s post-subtraction + * precision (~10 significant digits) at the crossover. + */ [[nodiscard]] Number computePowerMinusOneHybrid(Number const& periodicRate, std::uint32_t paymentsRemaining); +/** Compute the standard amortization payment factor `r(1+r)^n / ((1+r)^n - 1)`. + * + * Multiplying this factor by the outstanding principal yields the fixed periodic + * payment. Implements Equation (6) from XLS-66, Section A-2. + * + * When `fixCleanup3_2_0` is active the denominator is evaluated via + * `computePowerMinusOneHybrid()` to avoid catastrophic cancellation at near-zero + * rates. The pre-amendment path uses `power(1+r, n) - 1` directly and is + * preserved for historic replay. + * + * @param rules Active amendment rules (gates the hybrid path). + * @param periodicRate Per-period rate `r`; must be >= 0. + * @param paymentsRemaining Number of remaining payments `n`. + * @return The payment factor; `1/n` when `r == 0`; 0 when `n == 0`. + */ [[nodiscard]] Number computePaymentFactor( Rules const& rules, Number const& periodicRate, std::uint32_t paymentsRemaining); +/** Split a gross interest amount into net vault interest and broker management fee. + * + * Computes `fee = computeManagementFee(interest, managementFeeRate)` and + * returns `(interest - fee, fee)`. Implements Equation (33) from XLS-66, + * Section A-2 Equation Glossary. + * + * @param asset Asset used for rounding the fee. + * @param interest Gross interest amount to split. + * @param managementFeeRate Broker's share of gross interest in tenth-bips. + * @param loanScale Exponent for rounding the fee downward. + * @return Pair `(netInterest, fee)` where `netInterest + fee == interest`. + */ std::pair computeInterestAndFeeParts( Asset const& asset, @@ -384,6 +654,19 @@ computeInterestAndFeeParts( TenthBips16 managementFeeRate, std::int32_t loanScale); +/** Compute the fixed installment amount for a standard amortized loan. + * + * Implements `principal * paymentFactor(r, n)`. For zero-interest loans + * the formula degenerates to equal principal slices (`principal / n`). + * Implements Equation (7) from XLS-66, Section A-2 Equation Glossary. + * + * @param rules Active amendment rules (passed to `computePaymentFactor`). + * @param principalOutstanding Current outstanding principal. + * @param periodicRate Per-period interest rate. + * @param paymentsRemaining Number of payments remaining. + * @return Unrounded periodic payment; 0 if `principalOutstanding == 0` + * or `paymentsRemaining == 0`. + */ Number loanPeriodicPayment( Rules const& rules, @@ -391,6 +674,20 @@ loanPeriodicPayment( Number const& periodicRate, std::uint32_t paymentsRemaining); +/** Reverse-calculate the outstanding principal implied by a given periodic payment. + * + * The inverse of `loanPeriodicPayment()`: recovers what the principal should be + * at a given point in the amortization schedule. Used by + * `computeTheoreticalLoanState()` and the early-closure path. Implements + * Equation (10) from XLS-66, Section A-2 Equation Glossary. + * + * @param rules Active amendment rules (passed to `computePaymentFactor`). + * @param periodicPayment Fixed installment amount. + * @param periodicRate Per-period interest rate. + * @param paymentsRemaining Number of payments remaining. + * @return Theoretical outstanding principal; 0 if `paymentsRemaining == 0`; + * `periodicPayment * paymentsRemaining` when `periodicRate == 0`. + */ Number loanPrincipalFromPeriodicPayment( Rules const& rules, @@ -398,6 +695,19 @@ loanPrincipalFromPeriodicPayment( Number const& periodicRate, std::uint32_t paymentsRemaining); +/** Compute the penalty interest accrued on an overdue payment. + * + * Calculates `principal * loanPeriodicRate(lateInterestRate, secondsOverdue)`. + * Returns 0 if the payment is on time or early, if `principalOutstanding == 0`, + * or if `lateInterestRate == 0`. Implements Equation (16) from XLS-66, + * Section A-2 Equation Glossary. + * + * @param principalOutstanding Current outstanding principal. + * @param lateInterestRate Annualized penalty rate in tenth-of-a-basis-point units. + * @param parentCloseTime Close time of the parent ledger (the "now"). + * @param nextPaymentDueDate Timestamp when the payment was originally due. + * @return Unrounded late penalty interest; 0 if the payment is not overdue. + */ Number loanLatePaymentInterest( Number const& principalOutstanding, @@ -405,6 +715,22 @@ loanLatePaymentInterest( NetClock::time_point parentCloseTime, std::uint32_t nextPaymentDueDate); +/** Compute the interest that has accrued since the last payment. + * + * Prorates the periodic interest by the fraction of the payment interval that + * has elapsed since `prevPaymentDate` (or `startDate` for the first payment). + * Returns 0 if `principalOutstanding == 0`, `periodicRate == 0`, or the current + * time is before `startDate`. Implements Equation (27) from XLS-66, + * Section A-2 Equation Glossary. + * + * @param principalOutstanding Current outstanding principal. + * @param periodicRate Pre-computed per-period interest rate. + * @param parentCloseTime Close time of the parent ledger (the "now"). + * @param startDate Loan start date (epoch seconds). + * @param prevPaymentDate Due date of the most recently completed payment. + * @param paymentInterval Payment period length in seconds. + * @return Unrounded accrued interest; always >= 0. + */ Number loanAccruedInterest( Number const& principalOutstanding, @@ -414,6 +740,22 @@ loanAccruedInterest( std::uint32_t prevPaymentDate, std::uint32_t paymentInterval); +/** Compute payment components for a principal overpayment. + * + * Splits the overpayment into a tracked value delta (principal reduction), + * an untracked management fee for the broker, and an optional untracked + * interest charge for the vault. Implements Equations (20-22) from XLS-66, + * Section A-2 Equation Glossary. + * + * @param asset Loan asset for rounding. + * @param loanScale Rounding exponent. + * @param overpayment Extra principal amount paid above schedule. + * @param overpaymentInterestRate Rate applied to the overpayment as an interest + * charge; 0 means no interest on the extra principal. + * @param overpaymentFeeRate Fee rate applied to the overpayment for the broker. + * @param managementFeeRate Standard broker fee rate used to split interest. + * @return `ExtendedPaymentComponents` with `specialCase == Extra`. + */ ExtendedPaymentComponents computeOverpaymentComponents( Asset const& asset, @@ -423,6 +765,25 @@ computeOverpaymentComponents( TenthBips32 const overpaymentFeeRate, TenthBips16 const managementFeeRate); +/** Compute how a single scheduled installment splits into tracked components. + * + * Uses the theoretical loan state (from `computeTheoreticalLoanState()`) as a + * target and caps the deltas at the available balances and the periodic payment + * amount to avoid over-reducing the ledger fields. Implements + * `compute_payment_due()` from XLS-66 §3.2.4.4. + * + * @param rules Active amendment rules. + * @param asset Loan asset for rounding. + * @param scale Rounding exponent. + * @param totalValueOutstanding Current total value outstanding. + * @param principalOutstanding Current principal outstanding. + * @param managementFeeOutstanding Current management fee outstanding. + * @param periodicPayment Fixed unrounded installment amount. + * @param periodicRate Pre-computed per-period interest rate. + * @param paymentRemaining Number of payments still remaining. + * @param managementFeeRate Broker fee rate in tenth-bips. + * @return `PaymentComponents` with all tracked deltas set. + */ PaymentComponents computePaymentComponents( Rules const& rules, @@ -438,15 +799,44 @@ computePaymentComponents( } // namespace detail +/** Compute the component-wise difference between two loan states. + * + * @return `LoanStateDeltas` with each field equal to the corresponding + * `lhs` field minus the `rhs` field. + */ detail::LoanStateDeltas operator-(LoanState const& lhs, LoanState const& rhs); +/** Subtract a set of deltas from a loan state. + * + * @return New `LoanState` with each field reduced by the corresponding delta. + */ LoanState operator-(LoanState const& lhs, detail::LoanStateDeltas const& rhs); +/** Add a set of deltas to a loan state. + * + * @return New `LoanState` with each field increased by the corresponding delta. + */ LoanState operator+(LoanState const& lhs, detail::LoanStateDeltas const& rhs); +/** Compute all derived loan properties from raw interest-rate parameters. + * + * Convenience overload that converts `interestRate` and `paymentInterval` to a + * per-period rate via `loanPeriodicRate()` and delegates to the `periodicRate` + * overload. See that overload's documentation for the full algorithm. + * + * @param rules Active amendment rules. + * @param asset Loan asset. + * @param principalOutstanding Requested or remaining principal. + * @param interestRate Annual interest rate in tenth-of-a-basis-point units. + * @param paymentInterval Payment period length in seconds. + * @param paymentsRemaining Total number of scheduled payments. + * @param managementFeeRate Broker fee rate in tenth-bips. + * @param minimumScale Floor on the derived `loanScale`. + * @return `LoanProperties` suitable for `checkLoanGuards()`. + */ LoanProperties computeLoanProperties( Rules const& rules, @@ -458,6 +848,30 @@ computeLoanProperties( TenthBips32 managementFeeRate, std::int32_t minimumScale); +/** Compute all derived loan properties from a pre-converted periodic rate. + * + * Calculates `periodicPayment`, the rounded total value outstanding, the + * `loanScale` (derived from the `STAmount` exponent of the total value, + * clamped to `minimumScale`), and `firstPaymentPrincipal`. Implements + * concepts from XLS-66 §3.2.4.3 and Equations 30-33 from Section A-2. + * + * The `loanScale` is not fixed at loan creation — it is derived dynamically so + * that all subsequent rounding of principal, interest, and fees uses a + * consistent number of decimal places, preventing dust-accumulation bugs. + * + * Called at loan creation (`LoanSet::doApply()`) and after each overpayment + * re-amortization inside `detail::tryOverpayment()`. + * + * @param rules Active amendment rules. + * @param asset Loan asset. + * @param principalOutstanding Requested or remaining principal. + * @param periodicRate Pre-computed per-period interest rate. + * @param paymentsRemaining Total number of scheduled payments. + * @param managementFeeRate Broker fee rate in tenth-bips. + * @param minimumScale Floor on the derived `loanScale`. + * @return `LoanProperties` with all fields computed and ready for + * `checkLoanGuards()`. + */ LoanProperties computeLoanProperties( Rules const& rules, @@ -468,15 +882,63 @@ computeLoanProperties( TenthBips32 managementFeeRate, std::int32_t minimumScale); +/** Check whether a value is already rounded to the given scale. + * + * Compares the downward- and upward-rounded forms; equality means no + * sub-scale precision remains. Used as a precondition guard and + * post-condition assertion throughout the payment pipeline. + * + * @param asset Asset whose representable precision constrains rounding. + * @param value The value to test. + * @param scale Exponent that defines the target precision. + * @return `true` if `roundDown(value) == roundUp(value)` at `scale`. + */ bool isRounded(Asset const& asset, Number const& value, std::int32_t scale); -// Indicates what type of payment is being made. -// regular, late, and full are mutually exclusive. -// overpayment is an "add on" to a regular payment, and follows that path with -// potential extra work at the end. -enum class LoanPaymentType { Regular = 0, Late, Full, Overpayment }; +/** Classification of the payment being made in a `LoanPay` transaction. + * + * The values are mutually exclusive for `Regular`, `Late`, and `Full`. + * `Overpayment` is an add-on to a `Regular` payment: it follows the regular + * path and may trigger a re-amortization step at the end if excess funds remain. + */ +enum class LoanPaymentType { + Regular = 0, /**< Scheduled installment, paid on time. */ + Late, /**< Overdue installment, carries penalty interest. */ + Full, /**< Early full closure, may carry a prepayment penalty. */ + Overpayment /**< Regular payment with additional principal reduction. */ +}; +/** Execute a loan payment and return the breakdown of amounts disbursed. + * + * Top-level entry point called by `LoanPay::doApply()`. Dispatches to the + * appropriate internal calculation path based on `paymentType`: + * + * - **Regular / Overpayment**: loops up to `kLOAN_MAXIMUM_PAYMENTS_PER_TRANSACTION` + * times applying `computePaymentComponents()`. When `paymentType == Overpayment` + * and funds remain after all regular installments, + * `computeOverpaymentComponents()` + `detail::tryOverpayment()` handle the + * re-amortization step. + * - **Late**: calls `detail::computeLatePayment()` then commits. + * - **Full**: calls `detail::computeFullPayment()` then commits. + * + * Any overdue payment not flagged `Late` is rejected with `tecEXPIRED`. Loan + * completion (all balances zeroed) and schedule advancement are handled as part + * of committing each payment round. Implements `make_payment` from XLS-66 + * §3.2.4.4. + * + * @param asset Loan asset (for rounding and balance operations). + * @param view Apply view providing rules, parent close time, and SLE mutation. + * @param loan Mutable reference to the Loan SLE; updated in place. + * @param brokerSle Const reference to the LoanBroker SLE; supplies + * `sfManagementFeeRate`. + * @param amount Amount the borrower is supplying for this payment. + * @param paymentType One of `Regular`, `Late`, `Full`, or `Overpayment`. + * @param j Journal for diagnostic log messages. + * @return `Expected` with the payment breakdown on + * success; an error TER (`tecEXPIRED`, `tecINSUFFICIENT_PAYMENT`, + * `tecKILLED`, etc.) on failure. + */ Expected loanMakePayment( Asset const& asset, diff --git a/include/xrpl/ledger/helpers/MPTokenHelpers.h b/include/xrpl/ledger/helpers/MPTokenHelpers.h index 6544b18dd1..5ea5890740 100644 --- a/include/xrpl/ledger/helpers/MPTokenHelpers.h +++ b/include/xrpl/ledger/helpers/MPTokenHelpers.h @@ -1,3 +1,16 @@ +/** @file + * MPT-specific ledger helper declarations. + * + * Declares the MPT counterpart to `RippleStateHelpers.h`. The asset-agnostic + * `TokenHelpers.h` dispatchers route `MPTIssue`-typed calls here via + * `std::visit` on the `Asset` variant. In addition to the functions that + * mirror IOU trust-line semantics (freeze, transfer rate, holding lifecycle, + * authorization), this header exposes operations with no IOU equivalent: + * escrow accounting, DEX permission gating, supply-overflow arithmetic, and + * the two-phase authorization protocol specific to MPT. + * + * @see RippleStateHelpers.h, TokenHelpers.h + */ #pragma once #include @@ -20,15 +33,65 @@ namespace xrpl { // //------------------------------------------------------------------------------ +/** Check whether an entire MPT issuance is globally frozen. + * + * Reads the `MPTokenIssuance` SLE and tests `lsfMPTLocked`. A missing + * issuance SLE is treated as unfrozen. + * + * @param view The ledger state to query. + * @param mptIssue The MPT issuance to check. + * @return `true` if `lsfMPTLocked` is set on the issuance; `false` otherwise. + */ [[nodiscard]] bool isGlobalFrozen(ReadView const& view, MPTIssue const& mptIssue); +/** Check whether a specific account's MPToken holding is individually frozen. + * + * Reads the per-holder `MPToken` SLE and tests `lsfMPTLocked`. Returns + * `false` if no `MPToken` SLE exists for the account (i.e., the account + * holds no balance for this issuance). + * + * @param view The ledger state to query. + * @param account The account whose holding is checked. + * @param mptIssue The MPT issuance to check against. + * @return `true` if the account's `MPToken` carries `lsfMPTLocked`; + * `false` otherwise. + */ [[nodiscard]] bool isIndividualFrozen(ReadView const& view, AccountID const& account, MPTIssue const& mptIssue); +/** Check whether an account's access to an MPT issuance is frozen by any tier. + * + * Applies three checks in order: global issuance lock (`isGlobalFrozen`), + * per-account holding lock (`isIndividualFrozen`), and vault pseudo-account + * freeze (`isVaultPseudoAccountFrozen`). Short-circuits on the first match. + * + * @param view The ledger state to query. + * @param account The account to check. + * @param mptIssue The MPT issuance to check against. + * @param depth Recursion depth guard forwarded to `isVaultPseudoAccountFrozen`; + * bounds pathological nested-vault configurations (currently unreachable + * in practice, but defended against up to `maxAssetCheckDepth`). + * @return `true` if any freeze tier applies; `false` otherwise. + */ [[nodiscard]] bool isFrozen(ReadView const& view, AccountID const& account, MPTIssue const& mptIssue, int depth = 0); +/** Check whether any account in a set is frozen for an MPT issuance. + * + * Sequences checks across separate passes to minimize cost: the global freeze + * is tested once and short-circuits immediately; individual per-account locks + * are checked for every account before the more expensive vault + * pseudo-account recursion begins. + * + * @param view The ledger state to query. + * @param accounts The set of accounts to check. + * @param mptIssue The MPT issuance to check against. + * @param depth Recursion depth guard forwarded to `isVaultPseudoAccountFrozen`. + * @return `true` if the global freeze is set, or if any account carries an + * individual freeze, or if any account is a frozen vault pseudo-account; + * `false` otherwise. + */ [[nodiscard]] bool isAnyFrozen( ReadView const& view, @@ -42,10 +105,18 @@ isAnyFrozen( // //------------------------------------------------------------------------------ -/** Returns MPT transfer fee as Rate. Rate specifies - * the fee as fractions of 1 billion. For example, 1% transfer rate - * is represented as 1,010,000,000. - * @param issuanceID MPTokenIssuanceID of MPTTokenIssuance object +/** Convert the `sfTransferFee` field of an MPT issuance to the XRPL `Rate` type. + * + * `sfTransferFee` is a `uint16` in the range 0–50,000 representing 0–50% + * (units of 0.001%). The encoding maps to `Rate` via + * `1,000,000,000 + (10,000 × fee)`, so a 50,000 field value becomes + * `1,500,000,000` (50% surcharge over the gross). When `sfTransferFee` is + * absent, `parityRate` (1,000,000,000 — no fee) is returned. + * + * @param view The ledger state to query. + * @param issuanceID The `MPTokenIssuanceID` of the issuance. + * @return The transfer rate as a `Rate` value; `parityRate` when no fee is + * configured or the issuance SLE is absent. */ [[nodiscard]] Rate transferRate(ReadView const& view, MPTID const& issuanceID); @@ -56,6 +127,18 @@ transferRate(ReadView const& view, MPTID const& issuanceID); // //------------------------------------------------------------------------------ +/** Read-only pre-check: verify that an independent holding can be created. + * + * Validates two preconditions before `addEmptyHolding` mutates the ledger: + * the `MPTokenIssuance` must exist, and it must carry `lsfMPTCanTransfer`. + * Tokens without `lsfMPTCanTransfer` can only move directly between the + * issuer and counterparties, making independent holdings meaningless. + * + * @param view The ledger state to query. + * @param mptIssue The MPT issuance the caller wants to hold. + * @return `tesSUCCESS`, `tecOBJECT_NOT_FOUND` if the issuance SLE is absent, + * or `tecNO_AUTH` if `lsfMPTCanTransfer` is not set. + */ [[nodiscard]] TER canAddHolding(ReadView const& view, MPTIssue const& mptIssue); @@ -65,6 +148,33 @@ canAddHolding(ReadView const& view, MPTIssue const& mptIssue); // //------------------------------------------------------------------------------ +/** Core MPToken SLE lifecycle function — create, delete, or toggle authorization. + * + * Behavior depends on `holderID`: + * - `holderID` absent (`nullopt`): `account` is the holder. Without + * `tfMPTUnauthorize`, a new zero-balance `MPToken` SLE is created and + * inserted into the owner directory; the XRP reserve is enforced when + * `ownerCount >= 2` (same policy as trust lines). With `tfMPTUnauthorize`, + * the existing SLE is erased and the owner count decremented. + * - `holderID` set: `account` must be the issuance's issuer. The function + * toggles `lsfMPTAuthorized` on the holder's existing `MPToken` SLE. + * + * @param view The mutable ledger state. + * @param priorBalance XRP balance before this transaction; used only for the + * reserve check when creating a new holding (`holderID` absent and + * `tfMPTUnauthorize` not set). + * @param mptIssuanceID The issuance being authorized or deauthorized. + * @param account Submitting account: the holder (when `holderID` is absent) + * or the issuer (when `holderID` is set). + * @param journal Logging sink. + * @param flags Transaction flags; `tfMPTUnauthorize` selects the + * delete/deauthorize path. + * @param holderID When set, `account` is the issuer and this is the holder + * whose `lsfMPTAuthorized` flag is toggled. + * @return `tesSUCCESS`, `tecINSUFFICIENT_RESERVE` if reserves are too low, + * `tecDUPLICATE` if the holding already exists, or a `tef` code on + * invariant violations. + */ [[nodiscard]] TER authorizeMPToken( ApplyView& view, @@ -75,12 +185,31 @@ authorizeMPToken( std::uint32_t flags = 0, std::optional holderID = std::nullopt); -/** Check if the account lacks required authorization for MPT. +/** Preclaim (read-only) authorization check for an MPT holding. * - * requireAuth check is recursive for MPT shares in a vault, descending to - * assets in the vault, up to maxAssetCheckDepth recursion depth. This is - * purely defensive, as we currently do not allow such vaults to be created. - * WeakAuth intentionally allows missing MPTokens under MPToken V2. + * Issuers are always authorized. When `featureSingleAssetVault` is active, + * vault and `LoanBroker` pseudo-accounts are implicitly authorized, and the + * check recurses into the vault's underlying asset (bounded by `depth` + * vs. `kMAX_ASSET_CHECK_DEPTH`). Domain-based authorization via + * `credentials::validDomain` takes precedence over `lsfMPTAuthorized` when + * `sfDomainID` is present on the issuance — a passing domain check succeeds + * even if no `MPToken` SLE exists. + * + * `WeakAuth` intentionally permits a missing `MPToken` SLE; used in MPToken + * V2 flows where the SLE is created on demand during apply. + * + * @note The recursion through vault assets is purely defensive; the ledger + * does not currently permit nested-vault MPT configurations. + * @param view The ledger state to query (read-only; called in preclaim). + * @param mptIssue The MPT issuance being accessed. + * @param account The account requesting access. + * @param authType Controls leniency toward missing `MPToken` SLEs; + * `WeakAuth` allows a missing SLE, `StrongAuth`/`Legacy` require it. + * @param depth Current recursion depth; guards against theoretical infinite + * recursion through nested vault configurations. + * @return `tesSUCCESS` if authorized, `tecOBJECT_NOT_FOUND` if the issuance + * is absent, `tecNO_AUTH` if authorization fails, or `tecEXPIRED` if + * domain credentials have expired. */ [[nodiscard]] TER requireAuth( @@ -90,11 +219,25 @@ requireAuth( AuthType authType = AuthType::Legacy, int depth = 0); -/** Enforce account has MPToken to match its authorization. +/** Enforce account has MPToken to match its authorization (doApply phase). * - * Called from doApply - it will check for expired (and delete if found any) - * credentials matching DomainID set in MPTokenIssuance. Must be called if - * requireAuth(...MPTIssue...) returned tesSUCCESS or tecEXPIRED in preclaim. + * Must be called when `requireAuth` returned `tesSUCCESS` or `tecEXPIRED` + * during preclaim. Re-checks authorization and, if a `sfDomainID` is set on + * the issuance, runs `verifyValidDomain` (which deletes expired credentials + * as a side effect). When domain authorization succeeds but the account has + * no `MPToken` SLE, one is created on the fly using `priorBalance` for the + * XRP reserve check. + * + * @note Must not be called for the issuer account. + * @param view The mutable ledger state (called in doApply). + * @param mptIssuanceID The issuance being accessed. + * @param account The holder account; must not be the issuer. + * @param priorBalance XRP balance before this transaction; used when lazily + * allocating a new `MPToken` SLE for domain-authorized holders. + * @param j Logging sink. + * @return `tesSUCCESS`, `tecNO_AUTH` if not authorized, `tecEXPIRED` if + * credentials have expired, or `tecINSUFFICIENT_RESERVE` if the reserve + * check fails during on-demand SLE creation. */ [[nodiscard]] TER enforceMPTokenAuthorization( @@ -104,9 +247,20 @@ enforceMPTokenAuthorization( XRPAmount const& priorBalance, beast::Journal j); -/** Check if the destination account is allowed - * to receive MPT. Return tecNO_AUTH if it doesn't - * and tesSUCCESS otherwise. +/** Check whether a transfer between two accounts is permitted by the issuance. + * + * When `lsfMPTCanTransfer` is absent, third-party transfers are blocked. + * Transfers where either `from` or `to` is the issuer are always allowed, + * mirroring the IOU trust-line policy that lets issuers send and receive + * their own tokens unconditionally. + * + * @param view The ledger state to query. + * @param mptIssue The MPT issuance involved in the transfer. + * @param from The sending account. + * @param to The receiving account. + * @return `tesSUCCESS` if the transfer is permitted, `tecOBJECT_NOT_FOUND` + * if the issuance SLE is absent, or `tecNO_AUTH` if `lsfMPTCanTransfer` + * is unset and neither endpoint is the issuer. */ [[nodiscard]] TER canTransfer( @@ -115,8 +269,16 @@ canTransfer( AccountID const& from, AccountID const& to); -/** Check if Asset can be traded on DEX. return tecNO_PERMISSION - * if it doesn't and tesSUCCESS otherwise. +/** Check whether an asset may be traded on the DEX. + * + * Dispatches via `asset.visit`: XRP and IOU assets always succeed; for MPT, + * reads the issuance SLE and checks `lsfMPTCanTrade`. + * + * @param view The ledger state to query. + * @param asset The asset to check; non-MPT assets always pass. + * @return `tesSUCCESS` if trading is permitted, `tecOBJECT_NOT_FOUND` if + * the MPT issuance SLE is absent, or `tecNO_PERMISSION` if + * `lsfMPTCanTrade` is not set. */ [[nodiscard]] TER canTrade(ReadView const& view, Asset const& asset); @@ -127,6 +289,24 @@ canTrade(ReadView const& view, Asset const& asset); // //------------------------------------------------------------------------------ +/** Create a zero-balance `MPToken` holding for `accountID`. + * + * Short-circuits to `tesSUCCESS` when the caller is the issuer — issuers + * never hold a `MPToken` SLE for their own issuance. For all other accounts, + * delegates to `authorizeMPToken`, which enforces the XRP reserve requirement + * and inserts the SLE into the owner directory. Returns `tefINTERNAL` if the + * issuance SLE is missing or globally locked (invariant violations). + * + * @param view The mutable ledger state. + * @param accountID The account requesting the holding. + * @param priorBalance XRP balance before this transaction; forwarded to + * `authorizeMPToken` for the reserve check. + * @param mptIssue The MPT issuance to hold. + * @param journal Logging sink. + * @return `tesSUCCESS`, `tecDUPLICATE` if a holding already exists, + * `tecINSUFFICIENT_RESERVE` if reserves are too low, or `tefINTERNAL` + * on issuance-state invariant violations. + */ [[nodiscard]] TER addEmptyHolding( ApplyView& view, @@ -135,6 +315,23 @@ addEmptyHolding( MPTIssue const& mptIssue, beast::Journal journal); +/** Delete a zero-balance `MPToken` holding. + * + * Requires `sfMPTAmount` to be zero and, when `fixCleanup3_1_3` is enabled, + * `sfLockedAmount` to be zero as well; returns `tecHAS_OBLIGATIONS` otherwise. + * When `accountID` is the issuer and no `MPToken` SLE exists, returns + * `tesSUCCESS` immediately — the normal issuer state. Otherwise delegates to + * `authorizeMPToken` with `tfMPTUnauthorize` to erase the SLE and decrement + * the owner count. + * + * @param view The mutable ledger state. + * @param accountID The account whose holding is being removed. + * @param mptIssue The MPT issuance. + * @param journal Logging sink. + * @return `tesSUCCESS`, `tecOBJECT_NOT_FOUND` if no holding exists (and + * caller is not the issuer), or `tecHAS_OBLIGATIONS` if the holding + * carries a non-zero balance or locked amount. + */ [[nodiscard]] TER removeEmptyHolding( ApplyView& view, @@ -148,6 +345,22 @@ removeEmptyHolding( // //------------------------------------------------------------------------------ +/** Move MPT funds from a holder's spendable balance into escrow. + * + * Decrements `sfMPTAmount` and increments `sfLockedAmount` on the sender's + * `MPToken` SLE, then increments `sfLockedAmount` on the `MPTokenIssuance` + * SLE. `sfOutstandingAmount` on the issuance is deliberately left unchanged — + * escrowed tokens remain outstanding until the escrow completes and the + * recipient actually receives them. All arithmetic is guarded by + * `canSubtract`/`canAdd`. + * + * @param view The mutable ledger state. + * @param uGrantorID The account placing tokens in escrow; must not be the issuer. + * @param saAmount The MPT amount to lock; must be a valid `MPTIssue` amount. + * @param j Logging sink. + * @return `tesSUCCESS`, or a `tec`/`tef` error if the issuance or `MPToken` + * SLE is missing, the sender is the issuer, or an arithmetic guard fires. + */ TER lockEscrowMPT( ApplyView& view, @@ -155,6 +368,28 @@ lockEscrowMPT( STAmount const& saAmount, beast::Journal j); +/** Release MPT funds from escrow and credit the recipient. + * + * Decrements `sfLockedAmount` on both the sender's `MPToken` SLE and the + * `MPTokenIssuance` SLE by `grossAmount`. Then, depending on the receiver: + * - Receiver is a third party: `sfMPTAmount` on the receiver's `MPToken` is + * incremented by `netAmount`. + * - Receiver is the issuer: `sfOutstandingAmount` on the issuance is + * decremented by `netAmount` — tokens return to the issuer and retire. + * When `fixTokenEscrowV1` is enabled and `grossAmount > netAmount`, the fee + * difference is additionally subtracted from `sfOutstandingAmount` because + * the fee tokens are effectively burned. All arithmetic is guarded by + * `canSubtract`/`canAdd`. + * + * @param view The mutable ledger state. + * @param uGrantorID The escrow grantor; must not be the issuer. + * @param uGranteeID The escrow grantee (may be the issuer). + * @param netAmount The MPT amount credited to the receiver after fees. + * @param grossAmount The MPT amount unlocked from escrow (>= `netAmount`). + * @param j Logging sink. + * @return `tesSUCCESS`, or a `tec`/`tef` error on missing SLEs or + * arithmetic guard failure. + */ TER unlockEscrowMPT( ApplyView& view, @@ -164,6 +399,18 @@ unlockEscrowMPT( STAmount const& grossAmount, beast::Journal j); +/** Low-level primitive: insert a new `MPToken` SLE and link it into the owner directory. + * + * Inserts the SLE unconditionally without checking for duplicates, enforcing + * reserves, or verifying issuance validity. Callers must perform those checks + * before invoking this function. + * + * @param view The mutable ledger state. + * @param mptIssuanceID The issuance the token belongs to. + * @param account The account that will own the `MPToken`. + * @param flags Initial `sfFlags` value for the new SLE. + * @return `tesSUCCESS`, or `tecDIR_FULL` if the owner directory is full. + */ TER createMPToken( ApplyView& view, @@ -171,6 +418,21 @@ createMPToken( AccountID const& account, std::uint32_t const flags); +/** Idempotently ensure a `MPToken` holding exists for `holder`. + * + * Succeeds immediately if `holder` is the issuer or if the `MPToken` SLE + * already exists. Otherwise calls `createMPToken` and increments the owner + * count. Suitable for apply-phase callers that need to auto-create a holding + * without the full reserve and issuance validity checks performed by + * `addEmptyHolding`. + * + * @param view The mutable ledger state. + * @param mptIssue The MPT issuance the holder will hold. + * @param holder The account to receive the holding. + * @param j Logging sink. + * @return `tesSUCCESS`, `tecDIR_FULL` if the owner directory is full, or + * `tecINTERNAL` if the holder's account SLE is missing. + */ TER checkCreateMPT( xrpl::ApplyView& view, @@ -184,25 +446,62 @@ checkCreateMPT( // //------------------------------------------------------------------------------ -// MaximumAmount doesn't exceed 2**63-1 +/** Return the configured supply cap for an MPT issuance. + * + * Returns `sfMaximumAmount` when present, or `kMAX_MP_TOKEN_AMOUNT` (2^63−1) + * when the field is absent, representing an uncapped issuance. The result is + * always non-negative and fits in a `std::int64_t`. + * + * @param sleIssuance The `MPTokenIssuance` SLE to query. + * @return The maximum allowed outstanding amount. + */ std::int64_t maxMPTAmount(SLE const& sleIssuance); -// OutstandingAmount may overflow and available amount might be negative. -// But available amount is always <= |MaximumAmount - OutstandingAmount|. +/** Compute remaining issuance headroom from a pre-read SLE. + * + * Returns `maxMPTAmount(sleIssuance) - sfOutstandingAmount`. May transiently + * be negative when the payment engine is processing a path step that + * temporarily exceeds `MaximumAmount` under `AllowMPTOverflow::Yes`. + * + * @param sleIssuance The `MPTokenIssuance` SLE to query. + * @return Headroom as a signed 64-bit integer; may be negative. + */ std::int64_t availableMPTAmount(SLE const& sleIssuance); +/** Compute remaining issuance headroom by reading the SLE from the view. + * + * Convenience overload that performs the SLE lookup. Throws + * `std::runtime_error` if the issuance SLE is absent — a missing issuance at + * this call site indicates a ledger consistency failure rather than a user + * error. + * + * @param view The ledger state to query. + * @param mptID The `MPTID` of the issuance. + * @return Headroom as a signed 64-bit integer; may be negative. + * @throws std::runtime_error if the `MPTokenIssuance` SLE is absent. + */ std::int64_t availableMPTAmount(ReadView const& view, MPTID const& mptID); -/** Checks for two types of OutstandingAmount overflow during a send operation. - * 1. **Direct directSendNoFee (Overflow: No):** A true overflow check when - * `OutstandingAmount > MaximumAmount`. This threshold is used for direct - * 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. +/** Check whether crediting `sendAmount` would overflow the outstanding supply. + * + * Two distinct overflow thresholds are applied based on `allowOverflow`: + * 1. **`AllowMPTOverflow::No` (direct send):** Enforces the strict cap + * `OutstandingAmount + sendAmount ≤ MaximumAmount`. Used by + * `directSendNoFee` transactions that bypass the payment engine. + * 2. **`AllowMPTOverflow::Yes` (payment engine):** Raises the effective + * ceiling to `UINT64_MAX` to allow transient in-flight values that exceed + * `MaximumAmount` during path routing. A matching redemption step in the + * same transaction collapses the overshoot before settlement. + * + * @param sendAmount The proposed additional issuance; must be non-negative. + * @param outstandingAmount Current `sfOutstandingAmount` from the issuance SLE. + * @param maximumAmount The configured cap (`sfMaximumAmount` or + * `kMAX_MP_TOKEN_AMOUNT`). + * @param allowOverflow Selects which ceiling to apply. + * @return `true` if adding `sendAmount` would exceed the applicable limit. */ bool isMPTOverflow( @@ -211,18 +510,33 @@ isMPTOverflow( std::int64_t maximumAmount, AllowMPTOverflow allowOverflow); -/** - * Determine funds available for an issuer to sell in an issuer owned offer. - * Issuing step, which could be either MPTEndPointStep last step or BookStep's - * TakerPays may overflow OutstandingAmount. Redeeming step, in BookStep's - * TakerGets redeems the offer's owner funds, essentially balancing out - * the overflow, unless the offer's owner is the issuer. +/** Determine funds available for an issuer to sell in an issuer-owned DEX offer. + * + * During an issuing step (outbound from the issuer), the issuer's + * "available" balance is the remaining issuance headroom (`availableMPTAmount`) + * adjusted by `balanceHookSelfIssueMPT` to account for any amount already + * sold within the same payment. Without this hook, offer-crossing could + * allow the issuer to exceed `sfMaximumAmount` across parallel paths in the + * same transaction. + * + * @param view The ledger state to query. + * @param issue The MPT issuance for which to compute issuer funds. + * @return The effective amount the issuer can sell; zero if the issuance SLE + * is absent. */ [[nodiscard]] STAmount issuerFundsToSelfIssue(ReadView const& view, MPTIssue const& issue); -/** Facilitate tracking of MPT sold by an issuer owning MPT sell offer. - * See ApplyView::issuerSelfDebitHookMPT(). +/** Track MPT sold by an issuer that owns an MPT sell offer. + * + * Records the cumulative amount sold during the current payment step so that + * subsequent calls to `issuerFundsToSelfIssue` return a correctly reduced + * available balance. Delegates to `ApplyView::issuerSelfDebitHookMPT` after + * computing the current issuance headroom. + * + * @param view The mutable ledger state. + * @param issue The MPT issuance being sold. + * @param amount The additional amount sold in this step. */ void issuerSelfDebitHookMPT(ApplyView& view, MPTIssue const& issue, std::uint64_t amount); @@ -233,9 +547,26 @@ issuerSelfDebitHookMPT(ApplyView& view, MPTIssue const& issue, std::uint64_t amo // //------------------------------------------------------------------------------ -/* Return true if a transaction is allowed for the specified MPT/account. The - * function checks MPTokenIssuance and MPToken objects flags to determine if the - * transaction is allowed. +/** Comprehensive MPT transaction permission check for DEX and payment types. + * + * Verifies in order: the issuer account exists, the `MPTokenIssuance` SLE + * exists, the issuance is not globally locked (`lsfMPTLocked`), the + * `lsfMPTCanTrade` flag is set, and — for non-issuer accounts — that + * `lsfMPTCanTransfer` is set and the account's own `MPToken` is not + * individually locked. A missing `MPToken` SLE for a non-issuer is treated + * as passing: some transaction types create the `MPToken` on demand and + * perform their own missing-token checks. + * + * @note Must not be called with `txType == ttPAYMENT`; use the payment-engine + * path's own checks for payments. + * @param v The ledger state to query. + * @param tx The transaction type being gated. + * @param asset The asset involved; non-MPT assets always succeed. + * @param accountID The account initiating the transaction. + * @return `tesSUCCESS`, `tecOBJECT_NOT_FOUND` if the issuance is absent, + * `tecNO_ISSUER` if the issuer account is gone, `tecLOCKED` if the + * issuance or account is frozen, or `tecNO_PERMISSION` if trading or + * transfer is not permitted. */ TER checkMPTTxAllowed(ReadView const& v, TxType tx, Asset const& asset, AccountID const& accountID); diff --git a/include/xrpl/ledger/helpers/NFTokenHelpers.h b/include/xrpl/ledger/helpers/NFTokenHelpers.h index 4294e1ca13..c18b702b7d 100644 --- a/include/xrpl/ledger/helpers/NFTokenHelpers.h +++ b/include/xrpl/ledger/helpers/NFTokenHelpers.h @@ -1,3 +1,20 @@ +/** + * @file NFTokenHelpers.h + * @brief Core helpers for NFT paged-directory and offer management. + * + * Declares all mutable and read-only operations on the NFToken paged-directory + * structure and offer queues. Every transaction that touches an NFToken — + * minting, burning, transferring, or creating/cancelling offers — calls these + * helpers rather than manipulating ledger state directly. + * + * @note NFTs are packed into doubly-linked `ltNFTOKEN_PAGE` SLEs, each + * holding up to `kDIR_MAX_TOKENS_PER_PAGE` (32) tokens sorted by + * `compareTokens()`. Tokens sharing the same low-96-bit masked value + * (issuer + taxon) are *equivalent* and must be collocated on the same + * page. Page key invariant: every token's low 96 bits are strictly less + * than the low 96 bits of its enclosing page key. + */ + #pragma once #include @@ -13,18 +30,48 @@ namespace xrpl::nft { /** Delete up to a specified number of offers from the specified token offer - * directory. */ + * directory. + * + * Iterates the directory page-by-page, deleting offers in reverse index order + * within each page. Reverse iteration is required because `sfIndexes` is + * vector-backed and forward deletion would corrupt the remaining indices. + * Stops as soon as `maxDeletableOffers` offers have been removed. + * + * @param view The apply view to mutate. + * @param directory Keylet of the NFT buy or sell offer directory to drain. + * @param maxDeletableOffers Maximum number of offers to remove in this call. + * @return The number of offers actually deleted. + * @note Returns 0 immediately if `maxDeletableOffers` is 0. Used by + * `NFTokenBurn` to drain open offers within the per-transaction + * deletion cap (`maxDeletableTokenOfferEntries`). + */ std::size_t removeTokenOffersWithLimit( ApplyView& view, Keylet const& directory, std::size_t maxDeletableOffers); -/** Finds the specified token in the owner's token directory. */ +/** Finds the specified token in the owner's token directory. + * + * Read-only traversal: locates the `ltNFTOKEN_PAGE` candidate via `succ()` + * and searches the page's `sfNFTokens` array for a matching `sfNFTokenID`. + * + * @param view The read-only view to query. + * @param owner The account whose NFT directory is searched. + * @param nftokenID The 256-bit NFT identifier to look up. + * @return The matching token `STObject`, or `std::nullopt` if not found. + * @see findTokenAndPage for the mutable overload that also returns the page. + */ std::optional findToken(ReadView const& view, AccountID const& owner, uint256 const& nftokenID); -/** Finds the token in the owner's token directory. Returns token and page. */ +/** Token and its containing page, returned by `findTokenAndPage()`. + * + * Bundles the located token `STObject` with the mutable `shared_ptr` + * page so callers can modify the token in place without a second ledger + * traversal. The page pointer must be used exclusively on the same + * `ApplyView` that produced it. + */ struct TokenAndPage { STObject token; @@ -35,17 +82,81 @@ struct TokenAndPage { } }; + +/** Finds the token in the owner's token directory and returns it with its page. + * + * Mutable traversal via `ApplyView::peek()`. Returns both the token + * `STObject` and the `shared_ptr` page so that callers such as + * `NFTokenAcceptOffer` can pass the page directly to `removeToken()`, + * avoiding a redundant page lookup. + * + * @param view The apply view to query (mutable; uses `peek()`). + * @param owner The account whose NFT directory is searched. + * @param nftokenID The 256-bit NFT identifier to look up. + * @return A `TokenAndPage` containing the token and its page, or + * `std::nullopt` if the token is not found. + * @see findToken for the read-only alternative that returns only the token. + */ std::optional findTokenAndPage(ApplyView& view, AccountID const& owner, uint256 const& nftokenID); -/** Insert the token in the owner's token directory. */ +/** Insert the token in the owner's token directory. + * + * Locates or creates the appropriate `ltNFTOKEN_PAGE` via `getPageForToken()`. + * If the target page is full, it is split to make room; each split increments + * the owner's reserve count. Tokens are kept sorted within a page by + * `compareTokens()` (low 96-bit key first, full ID as tiebreaker). + * + * @param view The apply view to mutate. + * @param owner The account that will own the token. + * @param nft The token `STObject` to insert; must contain `sfNFTokenID`. + * @return `tesSUCCESS` on success, or `tecNO_SUITABLE_NFTOKEN_PAGE` if the + * target page is entirely filled with equivalent tokens (same low 96-bit + * key) and no split is possible. + */ TER insertToken(ApplyView& view, AccountID owner, STObject&& nft); -/** Remove the token from the owner's token directory. */ +/** Remove the token from the owner's token directory. + * + * Page-discovery overload: locates the containing `ltNFTOKEN_PAGE` via + * `succ()` and then delegates to the two-argument form. Use this when + * the caller does not already hold a page reference. + * + * After erasure, attempts to merge the affected page with its neighbours; + * each successful merge credits one reserve. If the page becomes empty it + * is unlinked and erased. + * + * @param view The apply view to mutate. + * @param owner The account that currently holds the token. + * @param nftokenID The 256-bit NFT identifier to remove. + * @return `tesSUCCESS`, or `tecNO_ENTRY` if the page or token cannot be + * found. + * @see removeToken(ApplyView&, AccountID const&, uint256 const&, shared_ptr const&) + * for the overload that skips the page lookup. + */ TER removeToken(ApplyView& view, AccountID const& owner, uint256 const& nftokenID); +/** Remove the token from the owner's token directory using a pre-located page. + * + * Caller-supplied page overload: skips the `succ()`-based page lookup when + * the caller already holds the page (e.g., from `findTokenAndPage()`). + * The `page` pointer must have been obtained from the same `ApplyView` + * instance. + * + * Under the `fixNFTokenPageLinks` amendment, if the emptied page is the final + * anchor page (`nftpage_max`), its contents are replaced with those of the + * previous page and the now-empty previous page is erased, preserving the + * invariant that the last page always has the stable sentinel key. + * + * @param view The apply view to mutate. + * @param owner The account that currently holds the token. + * @param nftokenID The 256-bit NFT identifier to remove. + * @param page The mutable SLE page known to contain the token. + * @return `tesSUCCESS`, or `tecNO_ENTRY` if the token is not found on the + * supplied page. + */ TER removeToken( ApplyView& view, @@ -53,28 +164,74 @@ removeToken( uint256 const& nftokenID, std::shared_ptr const& page); -/** Deletes the given token offer. - - An offer is tracked in two separate places: - - The token's 'buy' directory, if it's a buy offer; or - - The token's 'sell' directory, if it's a sell offer; and - - The owner directory of the account that placed the offer. - - The offer also consumes one incremental reserve. +/** Deletes the given token offer and removes it from both tracking directories. + * + * An offer is tracked in two separate places: + * - The token's `nft_buys` directory, if it is a buy offer; or + * - The token's `nft_sells` directory, if it is a sell offer; and + * - The owner's owner directory. + * + * Both directory entries are removed, the owner's reserve count is + * decremented by one, and the offer SLE is erased. + * + * @param view The apply view to mutate. + * @param offer The SLE for the offer to delete; must be of type + * `ltNFTOKEN_OFFER`. + * @return `true` if the offer was successfully deleted; `false` if the SLE + * is not of type `ltNFTOKEN_OFFER` or if a directory removal fails, + * acting as a type-safety guard. */ bool deleteTokenOffer(ApplyView& view, std::shared_ptr const& offer); -/** Repairs the links in an NFTokenPage directory. - - Returns true if a repair took place, otherwise false. -*/ +/** Repairs the links in an NFToken page directory. + * + * Walks the entire `ltNFTOKEN_PAGE` chain for the owner and corrects any + * broken `sfNextPageMin` / `sfPreviousPageMin` links. If the final page does + * not have the expected `nftpage_max` sentinel key, its contents are migrated + * to a newly created SLE with the correct key, the old SLE is erased, and the + * chain is relinked. Owner count is unchanged by this operation because the + * page count is preserved. + * + * Intended to be called by the `LedgerStateFix` transaction on accounts with + * known directory corruption. + * + * @param view The apply view to mutate. + * @param owner The account whose NFToken page directory is to be repaired. + * @return `true` if any correction was applied; `false` if the directory was + * already consistent. + */ bool repairNFTokenDirectoryLinks(ApplyView& view, AccountID const& owner); +/** Ordering predicate for NFToken IDs within and across pages. + * + * Sorts first by the low 96 bits of each ID (the `pageMask` region that + * determines page placement), then by the full 256-bit value as a + * tiebreaker. This ensures deterministic ordering for tokens that share + * the same low 96-bit prefix (equivalent tokens) and must co-reside on + * a single page. + * + * @param a First NFToken ID. + * @param b Second NFToken ID. + * @return `true` if `a` sorts before `b`. + */ bool compareTokens(uint256 const& a, uint256 const& b); +/** Modify the URI of an existing NFToken in the owner's directory. + * + * Locates the token's page and updates the `sfURI` field in the token's + * `STObject` within the page's `sfNFTokens` array. If `uri` is + * `std::nullopt`, the `sfURI` field is removed from the token. + * + * @param view The apply view to mutate. + * @param owner The account that owns the token. + * @param nftokenID The 256-bit NFT identifier whose URI is to be changed. + * @param uri The new URI value, or `std::nullopt` to clear the URI. + * @return `tesSUCCESS` on success, or `tecINTERNAL` if the page or token + * cannot be located (indicates ledger inconsistency). + */ TER changeTokenURI( ApplyView& view, @@ -82,7 +239,33 @@ changeTokenURI( uint256 const& nftokenID, std::optional const& uri); -/** Preflight checks shared by NFTokenCreateOffer and NFTokenMint */ +/** Preflight checks shared by NFTokenCreateOffer and NFTokenMint. + * + * Validates offer parameters that require no ledger access: negative or + * zero amounts (buy offers must carry a non-zero amount), zero IOU amounts, + * zero expiration, and malformed `owner`/`destination` combinations. + * A buy offer must supply `owner` (the targeted token holder); a sell offer + * must not (the seller is implicit). Neither party may designate itself as + * the destination. + * + * Defaults (`owner = nullopt`, `txFlags = tfSellNFToken`) allow + * `NFTokenMint` to reuse this path with minimal adaptation. + * + * @param acctID Account executing the transaction. + * @param amount The offer amount; must be non-negative and, for buy offers, + * non-zero and non-zero for IOUs. + * @param dest Optional destination account that may exclusively accept the + * offer; must not equal `acctID`. + * @param expiration Optional offer expiration; must not be zero. + * @param nftFlags The flags field of the NFToken being offered. + * @param rules Current ledger rule set used for amendment checks. + * @param owner For buy offers, the account that currently holds the token; + * must be absent for sell offers. + * @param txFlags Transaction flags; `tfSellNFToken` distinguishes sell from + * buy. + * @return `tesSUCCESS` if all static checks pass, or a `temXXX` error code + * indicating which parameter is invalid. + */ NotTEC tokenOfferCreatePreflight( AccountID const& acctID, @@ -94,7 +277,37 @@ tokenOfferCreatePreflight( std::optional const& owner = std::nullopt, std::uint32_t txFlags = tfSellNFToken); -/** Preclaim checks shared by NFTokenCreateOffer and NFTokenMint */ +/** Preclaim checks shared by NFTokenCreateOffer and NFTokenMint. + * + * Accesses the ledger to validate conditions that cannot be checked + * statically: + * - For non-XRP offers on tokens without `flagCreateTrustLines`, verifies + * that the NFT issuer's trust line for the IOU exists and is not frozen. + * Under `featureNFTokenMintOffer`, an issuer selling their own currency is + * exempt from this check. + * - Enforces `flagTransferable`: if absent and the transacting account is + * neither the issuer nor the current `sfNFTokenMinter`, returns + * `tefNFTOKEN_IS_NOT_TRANSFERABLE`. + * - For buy offers, verifies the account currently has sufficient funds. + * - Verifies `dest` and `owner` accounts exist and have not set + * `lsfDisallowIncomingNFTokenOffer`. + * - Under `fixEnforceNFTokenTrustlineV2`, calls `checkTrustlineAuthorized()` + * to reject offers backed by unauthorized trust lines that carry a balance. + * + * @param view The read-only ledger view. + * @param acctID Account executing the transaction. + * @param nftIssuer Issuer encoded in the NFToken ID. + * @param amount The offer amount. + * @param dest Optional restricted destination account. + * @param nftFlags The flags field of the NFToken being offered. + * @param xferFee Transfer fee encoded in the NFToken ID (basis points). + * @param j Journal for diagnostic logging. + * @param owner For buy offers, the account that currently holds the token. + * @param txFlags Transaction flags; `tfSellNFToken` distinguishes sell from + * buy. + * @return `tesSUCCESS` if all ledger-state checks pass, or a `tecXXX` / + * `tefXXX` error code. + */ TER tokenOfferCreatePreclaim( ReadView const& view, @@ -108,7 +321,28 @@ tokenOfferCreatePreclaim( std::optional const& owner = std::nullopt, std::uint32_t txFlags = tfSellNFToken); -/** doApply implementation shared by NFTokenCreateOffer and NFTokenMint */ +/** doApply implementation shared by NFTokenCreateOffer and NFTokenMint. + * + * Reserves XRP for the new `ltNFTOKEN_OFFER` object, inserts the offer into + * the account's owner directory and into the token's buy or sell directory + * (determined by `tfSellNFToken` in `txFlags`), constructs the SLE with the + * supplied fields, and increments the owner count. + * + * @param view The apply view to mutate. + * @param acctID Account executing the transaction. + * @param amount The offer amount. + * @param dest Optional restricted destination account. + * @param expiration Optional expiration time for the offer. + * @param seqProxy Sequence or ticket proxy used to derive the offer keylet. + * @param nftokenID The 256-bit ID of the NFToken being offered. + * @param priorBalance The account's XRP balance before the transaction fee + * was deducted; used to verify the reserve requirement. + * @param j Journal for diagnostic logging. + * @param txFlags Transaction flags; `tfSellNFToken` controls offer direction. + * @return `tesSUCCESS` on success, `tecINSUFFICIENT_RESERVE` if the account + * cannot cover the new object reserve, or `tecDIR_FULL` if either + * directory is at capacity. + */ TER tokenOfferCreateApply( ApplyView& view, @@ -122,6 +356,25 @@ tokenOfferCreateApply( beast::Journal j, std::uint32_t txFlags = tfSellNFToken); +/** Verify that an account is authorized to hold a given IOU trust line. + * + * Only active under the `fixEnforceNFTokenTrustlineV2` amendment; returns + * `tesSUCCESS` unconditionally when the amendment is not enabled. + * + * When active, checks that if the IOU issuer requires authorization + * (`lsfRequireAuth`), the trust line between `id` and the issuer exists and + * carries the appropriate `lsfLowAuth` / `lsfHighAuth` flag. The issuer + * account is always considered authorized to hold its own issuance. + * + * @param view The read-only ledger view. + * @param id The account whose authorization is being verified. + * @param j Journal for diagnostic logging. + * @param issue The IOU issue (currency + issuer) to check; must not be XRP. + * @return `tesSUCCESS` if authorized, `tecNO_ISSUER` if the issuer account + * does not exist, `tecNO_LINE` if the required trust line is absent, or + * `tecNO_AUTH` if the trust line exists but is not authorized. + * @note Only valid for custom (non-XRP) currencies; asserts otherwise. + */ TER checkTrustlineAuthorized( ReadView const& view, @@ -129,6 +382,26 @@ checkTrustlineAuthorized( beast::Journal const j, Issue const& issue); +/** Verify that an IOU trust line is not deep-frozen for a given account. + * + * Only active under the `featureDeepFreeze` amendment; returns + * `tesSUCCESS` unconditionally when the amendment is not enabled. + * + * When active, checks whether the trust line between `id` and the IOU issuer + * carries either `lsfLowDeepFreeze` or `lsfHighDeepFreeze`. Either side + * enacting deep freeze blocks token receipt, regardless of which party set it. + * The issuer account is always permitted to accept its own issuance; accounts + * with no trust line are treated as not frozen. + * + * @param view The read-only ledger view. + * @param id The account whose deep-freeze status is being checked. + * @param j Journal for diagnostic logging. + * @param issue The IOU issue (currency + issuer) to check; must not be XRP. + * @return `tesSUCCESS` if not deep-frozen or if no trust line exists, + * `tecNO_ISSUER` if the issuer account does not exist, or `tecFROZEN` + * if the trust line is deep-frozen. + * @note Only valid for custom (non-XRP) currencies; asserts otherwise. + */ TER checkTrustlineDeepFrozen( ReadView const& view, diff --git a/include/xrpl/ledger/helpers/OfferHelpers.h b/include/xrpl/ledger/helpers/OfferHelpers.h index 9096071811..c3f0015435 100644 --- a/include/xrpl/ledger/helpers/OfferHelpers.h +++ b/include/xrpl/ledger/helpers/OfferHelpers.h @@ -9,18 +9,38 @@ namespace xrpl { -/** Delete an offer. - - Requirements: - The offer must exist. - The caller must have already checked permissions. - - @param view The ApplyView to modify. - @param sle The offer to delete. - @param j Journal for logging. - - @return tesSUCCESS on success, otherwise an error code. -*/ +/** Remove an offer and its directory back-references from the ledger. + * + * Performs the full teardown sequence atomically within the transaction + * buffer: removes the offer from the owner's directory, removes it from + * the order-book quality directory, decrements the owner's reserve count, + * and erases the SLE. For hybrid offers (flagged `lsfHybrid`) that + * participate in one or more Permissioned DEX domains, each entry in + * `sfAdditionalBooks` is also removed from its domain-specific book + * directory before the owner-count adjustment and erasure. + * + * If `sle` is null the function returns `tesSUCCESS` immediately, + * allowing callers to pass the result of a failed `peek()` without + * a pre-check (defensive against double-delete within one batch). + * + * @pre The offer SLE must exist in the ledger and both its + * `sfOwnerNode` and `sfBookNode` back-references must be valid. + * @pre The caller must have already verified that the submitting + * account is authorized to delete this offer; this function + * performs no ownership or permission check. + * + * @param view The `ApplyView` transaction buffer to modify. + * @param sle The offer SLE to delete. May be null (treated as no-op). + * @param j Journal for diagnostic logging. + * + * @return `tesSUCCESS` on success, or `tefBAD_LEDGER` if a directory + * back-reference is missing (invariant violation; should not occur + * in a well-formed ledger). + * + * @note `[[nodiscard]]` is intentionally absent: `BookTip` and payment + * path callers do not always inspect the return value, and enforcing + * the attribute would have broken compilation across the engine. + */ // [[nodiscard]] // nodiscard commented out so Flow, BookTip and others compile. TER offerDelete(ApplyView& view, std::shared_ptr const& sle, beast::Journal j); diff --git a/include/xrpl/ledger/helpers/PaymentChannelHelpers.h b/include/xrpl/ledger/helpers/PaymentChannelHelpers.h index 24838f1331..021aae5a32 100644 --- a/include/xrpl/ledger/helpers/PaymentChannelHelpers.h +++ b/include/xrpl/ledger/helpers/PaymentChannelHelpers.h @@ -7,6 +7,35 @@ namespace xrpl { +/** Tear down a payment channel and return unspent XRP to its source account. + * + * Performs four ledger mutations in order: + * 1. Removes the channel from the source's owner directory (`sfOwnerNode`). + * 2. Conditionally removes the channel from the destination's owner directory + * (`sfDestinationNode`) — the field is absent on older channel objects that + * predate destination-directory tracking, so its presence is tested before + * the removal attempt. + * 3. Credits the unspent balance (`sfAmount - sfBalance`) back to the source + * account. `sfAmount` is the total XRP escrowed; `sfBalance` is the + * cumulative amount already paid to the destination. + * 4. Decrements the source's owner count and erases the `ltPAYCHAN` SLE. + * + * Called by both `PaymentChannelClaim` and `PaymentChannelFund` whenever a + * channel must be closed — on expiry (`cancelAfter`/`expiration` elapsed), on + * an explicit `tfClose` flag, or when the channel is fully drained. + * + * @param slep The `ltPAYCHAN` SLE to close; must satisfy + * `sfAmount >= sfBalance` (asserted). + * @param view The apply view through which all ledger mutations are made. + * @param key The ledger key of the channel SLE (used for directory removal). + * @param j Journal for fatal-level diagnostic messages on internal errors. + * @return `tesSUCCESS` on the normal path; `tefBAD_LEDGER` if an owner + * directory removal fails (indicates corrupted ledger state); + * `tefINTERNAL` if the source account SLE cannot be found. + * @note The `tefBAD_LEDGER` and `tefINTERNAL` branches are annotated + * `LCOV_EXCL` — they guard against ledger corruption that cannot occur + * during correct operation. + */ TER closeChannel( std::shared_ptr const& slep, diff --git a/include/xrpl/ledger/helpers/PermissionedDEXHelpers.h b/include/xrpl/ledger/helpers/PermissionedDEXHelpers.h index 04b12f2fc5..32bfed0136 100644 --- a/include/xrpl/ledger/helpers/PermissionedDEXHelpers.h +++ b/include/xrpl/ledger/helpers/PermissionedDEXHelpers.h @@ -1,13 +1,90 @@ +/** + * @file PermissionedDEXHelpers.h + * @brief Domain membership predicates for the Permissioned DEX. + * + * Declares the two authorization gatekeepers used by `xrpl::permissioned_dex` + * to enforce credential-based access control on restricted order books. + * Both functions are called from transaction preclaim logic and from live + * order-book traversal in `OfferStream`. + */ #pragma once #include namespace xrpl::permissioned_dex { -// Check if an account is in a permissioned domain +/** + * @brief Test whether an account currently qualifies as a member of a + * permissioned domain. + * + * Resolves the `PermissionedDomain` ledger object identified by @p domainID + * and applies a two-tier membership test: + * + * 1. **Owner shortcut** — the domain's `sfOwner` is always considered a member, + * avoiding a bootstrap problem where the owner couldn't trade in their own + * domain. + * 2. **Credential scan** — for all other accounts, the function iterates + * `sfAcceptedCredentials` and returns `true` as soon as it finds a + * credential issued to @p account that (a) carries the `lsfAccepted` flag + * and (b) has not expired according to `credentials::checkExpired` evaluated + * against the ledger's `parentCloseTime`. + * + * Expiry is evaluated against `parentCloseTime` (not wall time) so that all + * validators reach the same deterministic result regardless of local clock skew. + * + * @param view The read-only ledger view to query. + * @param account The account whose domain membership is being tested. + * @param domainID The identifier of the `PermissionedDomain` ledger object. + * @return `true` if @p account is the domain owner or holds at least one + * accepted, non-expired credential listed in the domain; `false` if the + * domain object does not exist, or if no qualifying credential is found. + * + * @note Called from `OfferCreate` preclaim (rejects with `tecNO_PERMISSION` if + * `false`) and twice from `Payment` preclaim — once for the sender, once for + * the destination — since a domain payment requires both parties to be + * members. Also called internally by `offerInDomain`. + */ [[nodiscard]] bool accountInDomain(ReadView const& view, AccountID const& account, Domain const& domainID); -// Check if an offer is in the permissioned domain +/** + * @brief Test whether a specific offer is still legitimately part of a + * permissioned domain at the time it is being consumed. + * + * Called by `OfferStream` during order-book traversal to handle the race + * between offer creation and subsequent credential expiry. An offer that was + * valid when placed may become invalid if the owner's credentials expire before + * the offer is matched. When this function returns `false`, `OfferStream` + * removes the offer from the book immediately (`permRmOffer`) instead of + * matching it. + * + * The function performs the following checks in order: + * - Offer SLE must exist (defensive; should not occur in a well-formed book). + * - Offer must carry `sfDomainID` (defensive; should not occur). + * - `sfDomainID` must match @p domainID (defensive; should not occur). + * - **Post-`fixCleanup3_1_3`**: a hybrid offer (`lsfHybrid`) must have + * `sfAdditionalBooks` present with exactly one entry; a violation is logged + * as an error and `false` is returned. + * - **Pre-`fixCleanup3_1_3`**: a hybrid offer must have `sfAdditionalBooks` + * present (size is not validated). + * - Delegates the final membership check to `accountInDomain` for the offer's + * owner (`sfAccount`). + * + * The three defensive checks are marked `LCOV_EXCL_LINE`; they guard against + * invariant violations that cannot occur under normal operation but are retained + * as safety nets. + * + * @param view The read-only ledger view to query. + * @param offerID The hash identifier of the offer SLE to validate. + * @param domainID The permissioned domain the offer is expected to belong to. + * @param j Journal used to log an error if a hybrid offer has a missing + * or malformed `sfAdditionalBooks` field. + * @return `true` if the offer passes all structural checks and its owner is + * currently a member of @p domainID; `false` otherwise. + * + * @note The `fixCleanup3_1_3` amendment tightens hybrid-offer validation from + * a presence-only check on `sfAdditionalBooks` to a presence-plus-size-one + * check. Both code paths must be preserved for deterministic historic replay. + */ [[nodiscard]] bool offerInDomain( ReadView const& view, diff --git a/include/xrpl/ledger/helpers/RippleStateHelpers.h b/include/xrpl/ledger/helpers/RippleStateHelpers.h index 17b0f7673e..827bb50535 100644 --- a/include/xrpl/ledger/helpers/RippleStateHelpers.h +++ b/include/xrpl/ledger/helpers/RippleStateHelpers.h @@ -1,3 +1,23 @@ +/** @file + * IOU trustline (RippleState) operations for the XRP Ledger. + * + * Declares every ledger operation that reads from or writes to a + * `RippleState` (trustline) SLE: credit-limit and balance queries, + * freeze checks, trustline lifecycle, IOU issuance/redemption, + * authorization and rippling enforcement, zero-balance holding + * management, and AMM-specific cleanup. + * + * This file is the IOU-specific leaf of the token helper layer. + * Asset-agnostic callers should go through the dispatchers in + * `TokenHelpers.h`, which branch on `Issue` vs `MPTIssue` and + * delegate here for the IOU path. + * + * @note The trustline orientation invariant is pervasive here: + * `sfLowLimit` always belongs to the account whose `AccountID` + * compares less; `sfHighLimit` to the other. Every function + * applies this flip internally — callers supply `(account, issuer)` + * and receive results in account-centric terms. + */ #pragma once #include @@ -10,27 +30,29 @@ #include #include -//------------------------------------------------------------------------------ -// -// RippleState (Trustline) helpers -// -//------------------------------------------------------------------------------ +// --- RippleState (Trustline) helpers --- namespace xrpl { -//------------------------------------------------------------------------------ -// -// Credit functions (from Credit.h) -// -//------------------------------------------------------------------------------ +// --- Credit queries --- -/** Calculate the maximum amount of IOUs that an account can hold - @param view the ledger to check against. - @param account the account of interest. - @param issuer the issuer of the IOU. - @param currency the IOU to check. - @return The maximum amount that can be held. -*/ +/** Read the maximum IOU balance that @p account has authorised @p issuer to + * carry on their behalf. + * + * Reads `sfLowLimit` or `sfHighLimit` from the trustline depending on + * which side `account` occupies (low if `account < issuer`). The issuer + * field of the returned amount is rewritten to `account` so the result is + * safe to consume without knowing the binary-ordering of the two accounts. + * Returns a zero-valued `STAmount` (with the correct issue) if no trustline + * exists. + * + * @param view Read-only ledger view to query. + * @param account The account whose credit limit is requested. + * @param issuer The IOU issuer. + * @param currency The currency of the trustline. + * @return The credit limit expressed from @p account's perspective, or zero + * if no trustline exists. + */ /** @{ */ STAmount creditLimit( @@ -39,16 +61,35 @@ creditLimit( AccountID const& issuer, Currency const& currency); +/** Convenience wrapper returning the credit limit as `IOUAmount`. + * + * @param v Read-only ledger view to query. + * @param acc The account whose credit limit is requested. + * @param iss The IOU issuer. + * @param cur The currency of the trustline. + * @return The credit limit as `IOUAmount`, or zero if no trustline exists. + * @see creditLimit + */ IOUAmount creditLimit2(ReadView const& v, AccountID const& acc, AccountID const& iss, Currency const& cur); /** @} */ -/** Returns the amount of IOUs issued by issuer that are held by an account - @param view the ledger to check against. - @param account the account of interest. - @param issuer the issuer of the IOU. - @param currency the IOU to check. -*/ +/** Read the IOU balance that @p account currently holds. + * + * `sfBalance` is stored in "low-account-sends-to-high-account" orientation. + * When `account` is the high side the stored value is negated before being + * returned, so callers always receive a balance expressed as "how much of + * this currency does @p account hold", regardless of which slot they occupy + * on the trustline. Returns zero (with the correct issue) if no trustline + * exists. + * + * @param view Read-only ledger view to query. + * @param account The account whose balance is requested. + * @param issuer The IOU issuer. + * @param currency The currency of the trustline. + * @return The balance expressed from @p account's perspective, or zero if + * no trustline exists. + */ /** @{ */ STAmount creditBalance( @@ -58,12 +99,20 @@ creditBalance( Currency const& currency); /** @} */ -//------------------------------------------------------------------------------ -// -// Freeze checking (IOU-specific) -// -//------------------------------------------------------------------------------ +// --- Freeze checks (IOU-specific) --- +/** Check whether @p issuer has individually frozen @p account's trustline. + * + * Inspects only the issuer's side flag (`lsfLowFreeze`/`lsfHighFreeze`) on + * the trustline. Does **not** check the issuer's global freeze flag — use + * `isFrozen` for that combined check. Always returns `false` for XRP. + * + * @param view Read-only ledger view. + * @param account The account to test. + * @param currency The IOU currency. + * @param issuer The IOU issuer. + * @return `true` if the issuer has set a line-level freeze on this account. + */ [[nodiscard]] bool isIndividualFrozen( ReadView const& view, @@ -71,12 +120,34 @@ isIndividualFrozen( Currency const& currency, AccountID const& issuer); +/** Convenience overload accepting an `Issue`. + * + * @param view Read-only ledger view. + * @param account The account to test. + * @param issue The IOU issue (currency + issuer). + * @return `true` if the issuer has set a line-level freeze on this account. + * @see isIndividualFrozen(ReadView const&, AccountID const&, Currency const&, + * AccountID const&) + */ [[nodiscard]] inline bool isIndividualFrozen(ReadView const& view, AccountID const& account, Issue const& issue) { return isIndividualFrozen(view, account, issue.currency, issue.account); } +/** Check whether @p account is frozen for @p currency issued by @p issuer. + * + * Returns `true` if either the issuer's `AccountRoot` has `lsfGlobalFreeze` + * set, or the issuer has frozen this specific trustline (`lsfLowFreeze` / + * `lsfHighFreeze`). Always returns `false` for XRP or when + * `account == issuer`. This is the check used by payment paths. + * + * @param view Read-only ledger view. + * @param account The account to test. + * @param currency The IOU currency. + * @param issuer The IOU issuer. + * @return `true` if the account cannot move this IOU due to any freeze. + */ [[nodiscard]] bool isFrozen( ReadView const& view, @@ -84,20 +155,52 @@ isFrozen( Currency const& currency, AccountID const& issuer); +/** Convenience overload accepting an `Issue`. + * + * @param view Read-only ledger view. + * @param account The account to test. + * @param issue The IOU issue (currency + issuer). + * @return `true` if the account cannot move this IOU due to any freeze. + * @see isFrozen(ReadView const&, AccountID const&, Currency const&, + * AccountID const&) + */ [[nodiscard]] inline bool isFrozen(ReadView const& view, AccountID const& account, Issue const& issue) { return isFrozen(view, account, issue.currency, issue.account); } -// Overload with depth parameter for uniformity with MPTIssue version. -// The depth parameter is ignored for IOUs since they don't have vault recursion. +/** Overload accepting a depth parameter for interface uniformity with MPT. + * + * IOUs do not have vault-level recursion, so the `depth` argument is + * unconditionally ignored. + * + * @param view Read-only ledger view. + * @param account The account to test. + * @param issue The IOU issue (currency + issuer). + * @return `true` if the account cannot move this IOU due to any freeze. + */ [[nodiscard]] inline bool isFrozen(ReadView const& view, AccountID const& account, Issue const& issue, int /*depth*/) { return isFrozen(view, account, issue); } +/** Check whether @p account is deep-frozen for @p currency issued by + * @p issuer. + * + * Deep-freeze (`lsfHighDeepFreeze` / `lsfLowDeepFreeze`) is a stricter + * condition than ordinary freeze: it prevents both sending *and* receiving + * the currency. Always returns `false` for XRP, and always returns `false` + * when `issuer == account` (an issuer cannot deep-freeze their own balance + * with themselves). + * + * @param view Read-only ledger view. + * @param account The account to test. + * @param currency The IOU currency. + * @param issuer The IOU issuer. + * @return `true` if the deep-freeze flag is set on either side of the line. + */ [[nodiscard]] bool isDeepFrozen( ReadView const& view, @@ -105,6 +208,18 @@ isDeepFrozen( Currency const& currency, AccountID const& issuer); +/** Convenience overload accepting an `Issue`, with an optional depth parameter + * for interface uniformity with the MPT equivalent. + * + * The `depth` argument is unconditionally ignored for IOUs. + * + * @param view Read-only ledger view. + * @param account The account to test. + * @param issue The IOU issue (currency + issuer). + * @return `true` if the deep-freeze flag is set on either side of the line. + * @see isDeepFrozen(ReadView const&, AccountID const&, Currency const&, + * AccountID const&) + */ [[nodiscard]] inline bool isDeepFrozen( ReadView const& view, @@ -115,22 +230,63 @@ isDeepFrozen( return isDeepFrozen(view, account, issue.currency, issue.account); } +/** Convert a deep-freeze check into a `TER` result. + * + * Convenience wrapper for transactor preflight code that returns + * `tecFROZEN` if the account is deep-frozen and `tesSUCCESS` otherwise. + * + * @param view Read-only ledger view. + * @param account The account to test. + * @param issue The IOU issue (currency + issuer). + * @return `tecFROZEN` if deep-frozen, `tesSUCCESS` otherwise. + */ [[nodiscard]] inline TER checkDeepFrozen(ReadView const& view, AccountID const& account, Issue const& issue) { return isDeepFrozen(view, account, issue) ? (TER)tecFROZEN : (TER)tesSUCCESS; } -//------------------------------------------------------------------------------ -// -// Trust line operations -// -//------------------------------------------------------------------------------ +// --- Trust line lifecycle --- -/** Create a trust line - - This can set an initial balance. -*/ +/** Create a new `RippleState` (trustline) SLE and insert it into both owner + * directories. + * + * This is the lowest-level entry point for trustline creation. It is called + * directly by `TrustSet` transactors and indirectly by `issueIOU` when the + * destination has no existing line. + * + * The function writes all trustline fields — limits, quality in/out, balance, + * and flag bits — using side-aware field selectors (`sfLowLimit`/`sfHighLimit` + * etc.) derived from `bSrcHigh`. The peer account's `lsfNoRipple` flag is + * initialised from the peer's `lsfDefaultRipple` setting (absent means + * noRipple is on by default). + * + * @param view Mutable ledger view. + * @param bSrcHigh `true` if `uSrcAccountID` occupies the "high" slot + * (i.e., `uSrcAccountID > uDstAccountID`). + * @param uSrcAccountID The account whose limit and flags are being + * configured. + * @param uDstAccountID The peer account on the other side of the line. + * @param uIndex Pre-calculated keylet key for the new SLE. + * @param sleAccount The `AccountRoot` SLE for the account being set + * (used to adjust owner count); must not be null. + * @param bAuth If `true`, set the authorization flag on the source + * side of the line. + * @param bNoRipple If `true`, set `lsfNoRipple` on the source side. + * @param bFreeze If `true`, set the freeze flag on the source side. + * @param bDeepFreeze If `true`, set the deep-freeze flag on the source + * side. + * @param saBalance Initial balance from the source account's + * perspective; the issuer field must be `noAccount()`. + * @param saLimit Credit limit for the source account; the issuer + * field must be `uSrcAccountID`. + * @param uQualityIn Quality-in override (0 = default/no override). + * @param uQualityOut Quality-out override (0 = default/no override). + * @param j Journal for trace/debug logging. + * @return `tesSUCCESS` on success, `tecDIR_FULL` if either owner directory + * is at capacity, `tecNO_TARGET` if the peer account does not exist, + * or `tefINTERNAL` if `sleAccount` is null or has a mismatched ID. + */ [[nodiscard]] TER trustCreate( ApplyView& view, @@ -151,6 +307,21 @@ trustCreate( std::uint32_t uQualityOut, beast::Journal j); +/** Delete a `RippleState` (trustline) SLE and remove its directory backlinks. + * + * Removes the SLE from both the low and high owner directories using the + * `sfLowNode`/`sfHighNode` deletion hints stored inside the SLE itself, + * then erases the SLE from the view. + * + * @param view Mutable ledger view. + * @param sleRippleState The trustline SLE to delete; must be obtained + * from `view.peek()`. + * @param uLowAccountID The account occupying the low slot. + * @param uHighAccountID The account occupying the high slot. + * @param j Journal for trace/debug logging. + * @return `tesSUCCESS` on success, `tefBAD_LEDGER` if either directory + * removal fails (indicating ledger corruption). + */ [[nodiscard]] TER trustDelete( ApplyView& view, @@ -159,12 +330,30 @@ trustDelete( AccountID const& uHighAccountID, beast::Journal j); -//------------------------------------------------------------------------------ -// -// IOU issuance/redemption -// -//------------------------------------------------------------------------------ +// --- IOU issuance/redemption --- +/** Issue IOUs from @p issue.account to @p account, adjusting the trustline + * balance. + * + * Debits the issuer's side of the trustline and credits the receiver. After + * adjusting the balance, calls the internal `updateTrustLine` helper: if the + * sender's balance crosses zero and seven specific cleanup conditions are met + * (zero limit, no freeze, etc.), the sender's reserve is released and the + * line may be deleted via `trustDelete`. + * + * If no trustline exists for the receiver, one is created via `trustCreate`, + * inheriting the receiver's `lsfDefaultRipple` setting for the initial + * `lsfNoRipple` state. Always invokes `view.creditHookIOU()` after mutating + * the balance. + * + * @param view Mutable ledger view. + * @param account The account receiving the IOUs (must not be the issuer). + * @param amount The amount to issue; its `Issue` must match @p issue. + * @param issue Identifies the currency and issuer. + * @param j Journal for trace/debug logging. + * @return `tesSUCCESS` on success, or a `tef`/`tec` code propagated from + * `trustCreate` or `trustDelete` if an error occurs. + */ [[nodiscard]] TER issueIOU( ApplyView& view, @@ -173,6 +362,26 @@ issueIOU( Issue const& issue, beast::Journal j); +/** Redeem IOUs held by @p account back toward the issuer, adjusting the + * trustline balance. + * + * The mirror image of `issueIOU`: credits the issuer and debits the holder. + * After adjusting the balance, calls `updateTrustLine` for the same + * automatic cleanup logic. Always invokes `view.creditHookIOU()` after + * mutating the balance. + * + * Unlike `issueIOU`, a missing trustline is treated as a fatal internal + * error (`tefINTERNAL`) because it is impossible to redeem a balance on a + * line that does not exist. + * + * @param view Mutable ledger view. + * @param account The account redeeming IOUs (must not be the issuer). + * @param amount The amount to redeem; its `Issue` must match @p issue. + * @param issue Identifies the currency and issuer. + * @param j Journal for trace/debug logging. + * @return `tesSUCCESS` on success, `tefINTERNAL` if no trustline exists, + * or a `tef`/`tec` code from `trustDelete` if cleanup triggers an error. + */ [[nodiscard]] TER redeemIOU( ApplyView& view, @@ -181,28 +390,30 @@ redeemIOU( Issue const& issue, beast::Journal j); -//------------------------------------------------------------------------------ -// -// Authorization and transfer checks (IOU-specific) -// -//------------------------------------------------------------------------------ +// --- Authorization and transfer checks (IOU-specific) --- -/** Check if the account lacks required authorization. +/** Check whether @p account is authorized to hold the IOU described by + * @p issue. * - * Return tecNO_AUTH or tecNO_LINE if it does - * and tesSUCCESS otherwise. + * Behaviour depends on @p authType: + * - **`StrongAuth`**: Returns `tecNO_LINE` immediately if no trustline + * exists. If the issuer has `lsfRequireAuth` and the line exists but is + * not authorized, returns `tecNO_AUTH`. + * - **`WeakAuth`** / **`Legacy`** (equivalent for IOUs): Returns + * `tecNO_AUTH` if `lsfRequireAuth` is set, the line exists, but is not + * authorized. Returns `tecNO_LINE` if auth is required and no line + * exists. If `lsfRequireAuth` is not set, returns `tesSUCCESS` even when + * no line exists — appropriate for payment path-finding where a line may + * be created on the fly. * - * If StrongAuth then return tecNO_LINE if the RippleState doesn't exist. Return - * tecNO_AUTH if lsfRequireAuth is set on the issuer's AccountRoot, and the - * RippleState does exist, and the RippleState is not authorized. + * Always returns `tesSUCCESS` for XRP or when `account == issue.account`. * - * If WeakAuth then return tecNO_AUTH if lsfRequireAuth is set, and the - * RippleState exists, and is not authorized. Return tecNO_LINE if - * lsfRequireAuth is set and the RippleState doesn't exist. Consequently, if - * WeakAuth and lsfRequireAuth is *not* set, this function will return - * tesSUCCESS even if RippleState does *not* exist. - * - * The default "Legacy" auth type is equivalent to WeakAuth. + * @param view Read-only ledger view. + * @param issue The IOU to check authorization for. + * @param account The account to check. + * @param authType Authorization strictness; defaults to `AuthType::Legacy` + * (equivalent to `WeakAuth` for IOUs). + * @return `tesSUCCESS`, `tecNO_AUTH`, or `tecNO_LINE`. */ [[nodiscard]] TER requireAuth( @@ -211,21 +422,53 @@ requireAuth( AccountID const& account, AuthType authType = AuthType::Legacy); -/** Check if the destination account is allowed - * to receive IOU. Return terNO_RIPPLE if rippling is - * disabled on both sides and tesSUCCESS otherwise. +/** Check whether an IOU can be transferred between @p from and @p to via the + * issuer's trustlines. + * + * Returns `tesSUCCESS` unconditionally when either endpoint is the issuer, + * or when the IOU is native (XRP). For third-party transfers, returns + * `terNO_RIPPLE` only when both the `from` and the `to` trustlines have + * `lsfNoRipple` set on the issuer's side, blocking rippling through. If a + * trustline does not exist for a given account, the issuer's + * `lsfDefaultRipple` flag is consulted as a fallback preference. + * + * @param view Read-only ledger view. + * @param issue The IOU (identifies the issuer and currency). + * @param from The sending account. + * @param to The receiving account. + * @return `tesSUCCESS` if the transfer is permitted, `terNO_RIPPLE` if + * rippling is disabled on both sides. */ [[nodiscard]] TER canTransfer(ReadView const& view, Issue const& issue, AccountID const& from, AccountID const& to); -//------------------------------------------------------------------------------ -// -// Empty holding operations (IOU-specific) -// -//------------------------------------------------------------------------------ +// --- Empty holding operations (IOU-specific) --- -/// Any transactors that call addEmptyHolding() in doApply must call -/// canAddHolding() in preflight with the same View and Asset +/** Create a zero-balance trustline for @p accountID, reserving the destination + * slot before any funds arrive. + * + * Used by transactors (e.g., DEX limit orders) that need to guarantee a + * destination line exists before settlement. Checks that @p accountID can + * cover the increased owner-count reserve before calling `trustCreate`. + * + * Returns `tesSUCCESS` immediately for XRP or when `accountID` is the + * issuer. Returns `tecDUPLICATE` if the trustline already exists. + * + * @note Any transactor that calls this function in `doApply` **must** call + * `canAddHolding()` (declared in `TokenHelpers.h`) in `preflight` with + * the same view and asset to validate the reserve precondition. + * + * @param view Mutable ledger view. + * @param accountID The account that will hold the IOU. + * @param priorBalance The account's XRP balance before the current + * transaction, used to test reserve sufficiency. + * @param issue The IOU to create a holding for. + * @param journal Journal for trace/debug logging. + * @return `tesSUCCESS` on success; `tecFROZEN` if the issuer is globally + * frozen; `tecNO_LINE_INSUF_RESERVE` if the account cannot afford the + * reserve; `tecDUPLICATE` if the line already exists; or a `tec`/`tef` + * code from `trustCreate`. + */ [[nodiscard]] TER addEmptyHolding( ApplyView& view, @@ -234,6 +477,20 @@ addEmptyHolding( Issue const& issue, beast::Journal journal); +/** Delete a zero-balance trustline previously created by `addEmptyHolding`. + * + * Validates that the balance is actually zero before deletion. Adjusts + * owner counts for both the low and high sides if their reserve flags are + * set, then calls `trustDelete`. + * + * @param view Mutable ledger view. + * @param accountID The account whose holding line should be removed. + * @param issue The IOU identifying the trustline to remove. + * @param journal Journal for trace/debug logging. + * @return `tesSUCCESS` on success; `tecHAS_OBLIGATIONS` if the balance is + * non-zero; `tecOBJECT_NOT_FOUND` if no line exists (and the account + * is not the issuer); or a `tef`/`tec` code from `trustDelete`. + */ [[nodiscard]] TER removeEmptyHolding( ApplyView& view, @@ -241,9 +498,27 @@ removeEmptyHolding( Issue const& issue, beast::Journal journal); -/** Delete trustline to AMM. The passed `sle` must be obtained from a prior - * call to view.peek(). Fail if neither side of the trustline is AMM or - * if ammAccountID is seated and is not one of the trustline's side. +/** Delete a trustline owned by an AMM pool account during AMM withdrawal. + * + * Validates that: + * - @p sleState is a non-null `ltRIPPLE_STATE` SLE. + * - Exactly one of the two trustline endpoints is an AMM account + * (identified by the presence of `sfAMMID` in the `AccountRoot`). + * - If @p ammAccountID is provided, it matches one of the endpoints. + * + * On success, calls `trustDelete` and decrements the owner count of the + * non-AMM side. + * + * @param view Mutable ledger view. + * @param sleState The `ltRIPPLE_STATE` SLE to delete; must be obtained + * from `view.peek()`. + * @param ammAccountID If provided, the expected AMM account ID; the + * function returns `terNO_AMM` if neither endpoint matches. + * @param j Journal for trace/debug logging. + * @return `tesSUCCESS` on success; `tecINTERNAL` if the SLE is null, has + * the wrong type, if both sides are AMM, or if the reserve flag is + * unexpectedly absent; `terNO_AMM` if neither endpoint is an AMM or + * the optional ID does not match; or a `tef` code from `trustDelete`. */ [[nodiscard]] TER deleteAMMTrustLine( @@ -252,8 +527,19 @@ deleteAMMTrustLine( std::optional const& ammAccountID, beast::Journal j); -/** Delete AMMs MPToken. The passed `sle` must be obtained from a prior - * call to view.peek(). +/** Delete an AMM account's `MPToken` SLE during AMM withdrawal. + * + * Removes the `MPToken` SLE from @p ammAccountID's owner directory and + * erases it from the view. The caller is responsible for any balance + * assertions before invoking this function. + * + * @param view Mutable ledger view. + * @param sleMPT The `MPToken` SLE to delete; must be obtained from + * `view.peek()`. + * @param ammAccountID The AMM account that owns the `MPToken`. + * @param j Journal for trace/debug logging. + * @return `tesSUCCESS` on success, `tefBAD_LEDGER` if the directory removal + * fails (indicating ledger corruption). */ [[nodiscard]] TER deleteAMMMPToken( diff --git a/include/xrpl/ledger/helpers/TokenHelpers.h b/include/xrpl/ledger/helpers/TokenHelpers.h index 3d41ac47cd..c70ea45c12 100644 --- a/include/xrpl/ledger/helpers/TokenHelpers.h +++ b/include/xrpl/ledger/helpers/TokenHelpers.h @@ -1,3 +1,20 @@ +/** @file + * Asset-agnostic dispatcher layer for all token operations on the XRP Ledger. + * + * This header is the unified entry point for token operations that must work + * across XRPL's three asset classes: XRP, IOU (trust-line-based), and MPT + * (Multi-Party Token). It sits between transaction-processing code that wants + * to be asset-agnostic and the two type-specific leaf modules: + * `RippleStateHelpers.h` for IOU trust lines and `MPTokenHelpers.h` for + * `MPToken`/`MPTokenIssuance` objects. + * + * Callers pass an `Asset` — a `std::variant` — and the + * functions here dispatch via `std::visit` or `Asset::visit` to the correct + * lower-level function, returning consistent result types (`STAmount`, `TER`, + * `bool`) regardless of asset kind. Adding a new asset type requires only + * extending the `Asset` variant and the branches here, not modifying call + * sites. + */ #pragma once #include @@ -20,30 +37,83 @@ namespace xrpl { // //------------------------------------------------------------------------------ -/** Controls the treatment of frozen account balances */ -enum class FreezeHandling { IgnoreFreeze, ZeroIfFrozen }; - -/** Controls the treatment of unauthorized MPT balances */ -enum class AuthHandling { IgnoreAuth, ZeroIfUnauthorized }; - -/** Controls whether to include the account's full spendable balance */ -enum class SpendableHandling { SimpleBalance, FullBalance }; - -enum class WaiveTransferFee : bool { No = false, Yes }; - -/** Controls whether accountSend is allowed to overflow OutstandingAmount **/ -enum class AllowMPTOverflow : bool { No = false, Yes }; - -/* Check if MPToken (for MPT) or trust line (for IOU) exists: - * - StrongAuth - before checking if authorization is required - * - WeakAuth - * for MPT - after checking lsfMPTRequireAuth flag - * for IOU - do not check if trust line exists - * - Legacy - * for MPT - before checking lsfMPTRequireAuth flag i.e. same as StrongAuth - * for IOU - do not check if trust line exists i.e. same as WeakAuth +/** Controls how a frozen balance is reported by balance-query functions. + * + * Use `ZeroIfFrozen` in payment paths where a frozen balance must not be + * spent. Use `IgnoreFreeze` in cleanup paths that need the real value + * regardless of freeze state. */ -enum class AuthType { StrongAuth, WeakAuth, Legacy }; +enum class FreezeHandling { + IgnoreFreeze, /**< Return the actual balance even if the holding is frozen. */ + ZeroIfFrozen /**< Return zero when the holding is frozen (the spendable amount). */ +}; + +/** Controls how an unauthorized MPT balance is reported by balance-query functions. + * + * Parallel to `FreezeHandling` but for MPT authorization. Use + * `ZeroIfUnauthorized` when computing the amount an account may legally spend. + */ +enum class AuthHandling { + IgnoreAuth, /**< Return the actual balance even if the MPToken is unauthorized. */ + ZeroIfUnauthorized /**< Return zero when the MPToken is not authorized. */ +}; + +/** Controls whether `accountHolds` reports simple or full spendable balance. + * + * - `SimpleBalance`: the amount the account can spend without going into + * debt, i.e. the raw trustline balance (negated to account-centric terms) + * for IOU, or the `sfMPTAmount` for MPT. + * - `FullBalance`: for IOU, also includes the peer's credit limit so the + * account can borrow up to that limit; for the IOU issuer, returns + * `STAmount::kMAX_VALUE`; for the MPT issuer, returns + * `MaximumAmount - OutstandingAmount`. + */ +enum class SpendableHandling { + SimpleBalance, /**< Balance the account can spend without going into debt. */ + FullBalance /**< Full spendable balance including borrowable credit or issuance capacity. */ +}; + +/** Controls whether the transfer fee is skipped during a send operation. + * + * Typed as `enum class : bool` to prevent accidental transposition with + * other boolean parameters at call sites. + */ +enum class WaiveTransferFee : bool { + No = false, /**< Apply the normal transfer fee. */ + Yes /**< Skip the transfer fee entirely. */ +}; + +/** Controls whether `accountSend` permits `OutstandingAmount` to transiently + * exceed `MaximumAmount` during MPT payment-engine routing. + * + * The payment engine issues tokens first (raising `OutstandingAmount`) and + * redeems them in the same transaction (lowering it back). `Yes` raises the + * overflow ceiling to `UINT64_MAX` for that transient window. Direct sends + * use `No` and enforce the strict `MaximumAmount` cap. + */ +enum class AllowMPTOverflow : bool { + No = false, /**< Enforce the strict MaximumAmount cap. */ + Yes /**< Allow transient overflow up to UINT64_MAX during routing. */ +}; + +/** Encodes the three-way authorization-strictness contract. + * + * Determines how `requireAuth` behaves when checking whether an account may + * hold or interact with a token: + * - `StrongAuth` checks that the holding object (trust line or `MPToken`) + * exists *before* asking whether authorization is set. Returns `tecNO_LINE` + * immediately if no holding exists. + * - `WeakAuth` skips the existence check, returning `tesSUCCESS` when + * authorization is not required even if no holding exists. Appropriate for + * payment path-finding where a line may be created on the fly. + * - `Legacy` maps to `StrongAuth` for MPT and `WeakAuth` for IOU, preserving + * historical behavior at existing call sites. + */ +enum class AuthType { + StrongAuth, /**< Existence of the holding object is verified first. */ + WeakAuth, /**< Holding existence is not required when auth is not needed. */ + Legacy /**< StrongAuth for MPT; WeakAuth for IOU (historical default). */ +}; //------------------------------------------------------------------------------ // @@ -51,35 +121,126 @@ enum class AuthType { StrongAuth, WeakAuth, Legacy }; // //------------------------------------------------------------------------------ +/** Check whether the issuer of @p asset has activated a global freeze. + * + * Dispatches to the IOU or MPT leaf based on the runtime type of @p asset. + * A global freeze on the issuer's `AccountRoot` blocks all holders + * simultaneously. + * + * @param view Read-only ledger view. + * @param asset The asset to test. + * @return `true` if the issuer has a global freeze in effect. + */ [[nodiscard]] bool isGlobalFrozen(ReadView const& view, Asset const& asset); +/** Check whether @p account has an individual freeze on @p asset. + * + * Dispatches to the IOU or MPT leaf based on the runtime type of @p asset. + * For IOU, checks the issuer's per-line freeze flag. For MPT, checks the + * `lsfMPTLocked` flag on the `MPToken` SLE. Does not check global freeze. + * + * @param view Read-only ledger view. + * @param account The account to test. + * @param asset The asset to test. + * @return `true` if the issuer has set an individual freeze on this account. + */ [[nodiscard]] bool isIndividualFrozen(ReadView const& view, AccountID const& account, Asset const& asset); -/** - * isFrozen check is recursive for MPT shares in a vault, descending to - * assets in the vault, up to maxAssetCheckDepth recursion depth. This is - * purely defensive, as we currently do not allow such vaults to be created. +/** Check whether @p account is frozen for @p asset (global or individual). + * + * Returns `true` if either `isGlobalFrozen` or `isIndividualFrozen` is true + * for the given account and asset. Dispatches to the typed IOU or MPT leaf + * via `std::visit`. + * + * The `depth` parameter enables recursive vault checking: if @p asset is an + * MPT backed by a vault, the vault's underlying asset is checked up to + * `maxAssetCheckDepth` levels deep. + * + * @note Recursion is purely defensive. The ledger currently does not allow + * nested vaults to be created, so `depth > 0` should not occur in + * practice. + * + * @param view Read-only ledger view. + * @param account The account to test. + * @param asset The asset to test. + * @param depth Current recursion depth for vault checking; defaults to 0. + * @return `true` if the account cannot move this asset due to any freeze. */ [[nodiscard]] bool isFrozen(ReadView const& view, AccountID const& account, Asset const& asset, int depth = 0); +/** Convert a freeze check on an IOU to a `TER`. + * + * Returns `tecFROZEN` if `isFrozen` is true for the given account and issue, + * `tesSUCCESS` otherwise. + * + * @param view Read-only ledger view. + * @param account The account to test. + * @param issue The IOU to test. + * @return `tecFROZEN` if frozen, `tesSUCCESS` otherwise. + */ [[nodiscard]] TER checkFrozen(ReadView const& view, AccountID const& account, Issue const& issue); +/** Convert a freeze check on an MPT to a `TER`. + * + * Returns `tecLOCKED` (not `tecFROZEN`) if `isFrozen` is true for the given + * account and MPT issuance, `tesSUCCESS` otherwise. The distinct error code + * reflects the separate protocol semantics of MPT locking vs IOU freezing. + * + * @param view Read-only ledger view. + * @param account The account to test. + * @param mptIssue The MPT issuance to test. + * @return `tecLOCKED` if frozen/locked, `tesSUCCESS` otherwise. + */ [[nodiscard]] TER checkFrozen(ReadView const& view, AccountID const& account, MPTIssue const& mptIssue); +/** Convert a freeze check on any asset to a `TER`. + * + * Dispatches to `checkFrozen(…, Issue)` or `checkFrozen(…, MPTIssue)` based + * on the runtime type of @p asset, returning the type-appropriate error code + * (`tecFROZEN` for IOU, `tecLOCKED` for MPT). + * + * @param view Read-only ledger view. + * @param account The account to test. + * @param asset The asset to test. + * @return `tecFROZEN` (IOU) or `tecLOCKED` (MPT) if frozen, `tesSUCCESS` + * otherwise. + */ [[nodiscard]] TER checkFrozen(ReadView const& view, AccountID const& account, Asset const& asset); +/** Check whether any account in @p accounts is frozen for @p issue. + * + * Iterates the list and returns `true` on the first frozen account. Used to + * check both sides (taker and maker) of an offer with a single call. + * + * @param view Read-only ledger view. + * @param accounts The accounts to test, e.g. `{takerID, makerID}`. + * @param issue The IOU to test. + * @return `true` if any account in the list is frozen for @p issue. + */ [[nodiscard]] bool isAnyFrozen( ReadView const& view, std::initializer_list const& accounts, Issue const& issue); +/** Check whether any account in @p accounts is frozen for @p asset. + * + * Asset-dispatching overload. Delegates to the IOU or MPT leaf for each + * account in the list. The `depth` parameter passes through to `isFrozen` + * for vault-backed MPT recursion. + * + * @param view Read-only ledger view. + * @param accounts The accounts to test. + * @param asset The asset to test. + * @param depth Recursion depth for vault checking; defaults to 0. + * @return `true` if any account in the list is frozen for @p asset. + */ [[nodiscard]] bool isAnyFrozen( ReadView const& view, @@ -87,6 +248,22 @@ isAnyFrozen( Asset const& asset, int depth = 0); +/** Check whether @p account is deep-frozen for @p mptIssue. + * + * For MPT, deep-freeze semantics are identical to regular freeze: a frozen + * MPT holder cannot send or receive. This function delegates to + * `isFrozen(view, account, mptIssue, depth)`. + * + * @note For IOU, deep-freeze is a distinct state (`lsfDeepFreeze`) where the + * holder cannot send but can still receive. See `isDeepFrozen` in + * `RippleStateHelpers.h` for IOU-specific semantics. + * + * @param view Read-only ledger view. + * @param account The account to test. + * @param mptIssue The MPT issuance to test. + * @param depth Recursion depth for vault checking; defaults to 0. + * @return `true` if the account is frozen/locked for this MPT. + */ [[nodiscard]] bool isDeepFrozen( ReadView const& view, @@ -94,17 +271,51 @@ isDeepFrozen( MPTIssue const& mptIssue, int depth = 0); -/** - * isFrozen check is recursive for MPT shares in a vault, descending to - * assets in the vault, up to maxAssetCheckDepth recursion depth. This is - * purely defensive, as we currently do not allow such vaults to be created. +/** Check whether @p account is deep-frozen for @p asset. + * + * Dispatches to the IOU or MPT leaf via `std::visit`. For MPT, deep-freeze + * is equivalent to regular freeze. For IOU, checks the `lsfDeepFreeze` flag, + * which prevents sending but allows receiving. + * + * The `depth` parameter enables recursive vault checking up to + * `maxAssetCheckDepth` levels. + * + * @note Recursion is purely defensive — nested vaults cannot currently be + * created on the ledger. + * + * @param view Read-only ledger view. + * @param account The account to test. + * @param asset The asset to test. + * @param depth Recursion depth for vault checking; defaults to 0. + * @return `true` if the account is deep-frozen for @p asset. */ [[nodiscard]] bool isDeepFrozen(ReadView const& view, AccountID const& account, Asset const& asset, int depth = 0); +/** Convert a deep-freeze check on an MPT to a `TER`. + * + * Returns `tecLOCKED` if `isDeepFrozen` is true, `tesSUCCESS` otherwise. + * + * @param view Read-only ledger view. + * @param account The account to test. + * @param mptIssue The MPT issuance to test. + * @return `tecLOCKED` if deep-frozen, `tesSUCCESS` otherwise. + */ [[nodiscard]] TER checkDeepFrozen(ReadView const& view, AccountID const& account, MPTIssue const& mptIssue); +/** Convert a deep-freeze check on any asset to a `TER`. + * + * Dispatches to `checkDeepFrozen(…, Issue)` (`tecFROZEN`) or + * `checkDeepFrozen(…, MPTIssue)` (`tecLOCKED`) based on the runtime type of + * @p asset. + * + * @param view Read-only ledger view. + * @param account The account to test. + * @param asset The asset to test. + * @return `tecFROZEN` (IOU) or `tecLOCKED` (MPT) if deep-frozen, + * `tesSUCCESS` otherwise. + */ [[nodiscard]] TER checkDeepFrozen(ReadView const& view, AccountID const& account, Asset const& asset); @@ -114,19 +325,31 @@ checkDeepFrozen(ReadView const& view, AccountID const& account, Asset const& ass // //------------------------------------------------------------------------------ -// Returns the amount an account can spend. -// -// If shSIMPLE_BALANCE is specified, this is the amount the account can spend -// without going into debt. -// -// If shFULL_BALANCE is specified, this is the amount the account can spend -// total. Specifically: -// * The account can go into debt if using a trust line, and the other side has -// a non-zero limit. -// * If the account is the asset issuer the limit is defined by the asset / -// issuance. -// -// <-- saAmount: amount of currency held by account. May be negative. +/** Return the amount that @p account can spend of the given currency/issuer. + * + * This is the canonical implementation. All other `accountHolds` overloads + * ultimately delegate here for the IOU path. + * + * - For XRP: returns `xrpLiquid(view, account, 0, j)` (reserve-adjusted). + * - For IOU with `shFULL_BALANCE` when `account == issuer`: returns + * `STAmount::kMAX_VALUE` — the issuer has effectively unlimited issuance + * capacity. + * - For IOU otherwise: reads the trust-line balance from the ledger, + * negating it to account-centric terms. If `shFULL_BALANCE` is specified, + * also adds the peer's credit limit so the account can draw down that + * credit. Returns zero if the line is frozen (when `ZeroIfFrozen`) or does + * not exist. + * + * @param view Read-only ledger view. + * @param account The account whose balance is queried. + * @param currency The IOU currency. + * @param issuer The IOU issuer. + * @param zeroIfFrozen Whether to return zero for frozen balances. + * @param j Journal for trace logging. + * @param includeFullBalance Whether to include borrowable credit or max + * issuance capacity; defaults to `SimpleBalance`. + * @return The spendable balance, which may be negative (e.g. trust-line debt). + */ [[nodiscard]] STAmount accountHolds( ReadView const& view, @@ -137,6 +360,19 @@ accountHolds( beast::Journal j, SpendableHandling includeFullBalance = SpendableHandling::SimpleBalance); +/** Return the spendable balance of an IOU for @p account. + * + * Convenience adapter over the `(Currency, AccountID)` overload, extracting + * the currency and issuer from @p issue. + * + * @param view Read-only ledger view. + * @param account The account whose balance is queried. + * @param issue The IOU (currency + issuer). + * @param zeroIfFrozen Whether to return zero for frozen balances. + * @param j Journal for trace logging. + * @param includeFullBalance Balance mode; defaults to `SimpleBalance`. + * @return The spendable balance from @p account's perspective. + */ [[nodiscard]] STAmount accountHolds( ReadView const& view, @@ -146,6 +382,29 @@ accountHolds( beast::Journal j, SpendableHandling includeFullBalance = SpendableHandling::SimpleBalance); +/** Return the spendable balance of an MPT for @p account. + * + * - For the MPT issuer with `shFULL_BALANCE`: returns + * `MaximumAmount - OutstandingAmount` (available issuance capacity) via + * `availableMPTAmount`. + * - For regular holders: reads `sfMPTAmount` from the `MPToken` SLE. Returns + * zero if: the `MPToken` SLE does not exist; the token is frozen and + * `ZeroIfFrozen` is set; or the token is unauthorized and + * `ZeroIfUnauthorized` is set (with `featureSingleAssetVault` gating the + * precise auth-check path). + * - Under `featureMPTokensV2`, the result passes through + * `view.balanceHookMPT` to allow `PaymentSandbox` deferred-credit + * interception. + * + * @param view Read-only ledger view. + * @param account The account whose balance is queried. + * @param mptIssue The MPT issuance. + * @param zeroIfFrozen Whether to zero the balance when frozen/locked. + * @param zeroIfUnauthorized Whether to zero the balance when unauthorized. + * @param j Journal for trace logging. + * @param includeFullBalance Balance mode; defaults to `SimpleBalance`. + * @return The spendable MPT balance, or zero per the policy flags above. + */ [[nodiscard]] STAmount accountHolds( ReadView const& view, @@ -156,6 +415,22 @@ accountHolds( beast::Journal j, SpendableHandling includeFullBalance = SpendableHandling::SimpleBalance); +/** Return the spendable balance of any asset for @p account. + * + * Asset-dispatching overload. Delegates to the `Issue` overload (which + * ignores `zeroIfUnauthorized`) or the `MPTIssue` overload based on the + * runtime type of @p asset. + * + * @param view Read-only ledger view. + * @param account The account whose balance is queried. + * @param asset The asset to query. + * @param zeroIfFrozen Whether to zero the balance when frozen. + * @param zeroIfUnauthorized Whether to zero the balance when unauthorized + * (MPT only; ignored for IOU). + * @param j Journal for trace logging. + * @param includeFullBalance Balance mode; defaults to `SimpleBalance`. + * @return The spendable balance per the policy flags. + */ [[nodiscard]] STAmount accountHolds( ReadView const& view, @@ -166,11 +441,29 @@ accountHolds( beast::Journal j, SpendableHandling includeFullBalance = SpendableHandling::SimpleBalance); -// Returns the amount an account can spend of the currency type saDefault, or -// returns saDefault if this account is the issuer of the currency in -// question. Should be used in favor of accountHolds when questioning how much -// an account can spend while also allowing currency issuers to spend -// unlimited amounts of their own currency (since they can always issue more). +/** Return how much of @p saDefault's currency @p id can fund, treating the + * issuer as having unlimited supply of their own currency. + * + * For IOU: if `id == saDefault.getIssuer()`, returns `saDefault` directly — + * the issuer can always fund an offer for their own currency up to whatever + * amount they specify. Otherwise delegates to `accountHolds` with + * `SimpleBalance`. + * + * This is the correct semantic for offer matching; prefer `accountFunds` over + * `accountHolds` when asking "can this account fund this offer?". + * + * @note `saDefault` must hold an `Issue` (not MPT). Use the `AuthHandling` + * overload for asset-agnostic callers. + * + * @param view Read-only ledger view. + * @param id The account to query. + * @param saDefault The amount (currency + issuer) to check fundability + * for. + * @param freezeHandling Whether to zero the balance when frozen. + * @param j Journal for trace logging. + * @return `saDefault` if @p id is the issuer; otherwise the trust-line + * balance, zeroed per @p freezeHandling. + */ [[nodiscard]] STAmount accountFunds( ReadView const& view, @@ -179,7 +472,22 @@ accountFunds( FreezeHandling freezeHandling, beast::Journal j); -// Overload with AuthHandling to support IOU and MPT. +/** Asset-agnostic overload of `accountFunds` supporting both IOU and MPT. + * + * For IOU: delegates to the `FreezeHandling`-only overload above. + * For MPT: delegates to `accountHolds` with `shFULL_BALANCE`, which + * returns the issuer's available issuance capacity or the holder's + * `sfMPTAmount`. + * + * @param view Read-only ledger view. + * @param id The account to query. + * @param saDefault The amount (currency/asset + issuer) to check. + * @param freezeHandling Whether to zero the balance when frozen. + * @param authHandling Whether to zero the balance when unauthorized (MPT + * only). + * @param j Journal for trace logging. + * @return The fundable balance per the policy flags. + */ [[nodiscard]] STAmount accountFunds( ReadView const& view, @@ -189,9 +497,15 @@ accountFunds( AuthHandling authHandling, beast::Journal j); -/** Returns the transfer fee as Rate based on the type of token - * @param view The ledger view - * @param amount The amount to transfer +/** Return the transfer fee for the asset embedded in @p amount. + * + * Dispatches on `amount.asset()`: for IOU, reads the issuer's transfer rate + * from their `AccountRoot`; for MPT, reads the `sfTransferFee` field from + * the `MPTokenIssuance` SLE. Both paths return a `Rate` (parts-per-billion). + * + * @param view Read-only ledger view. + * @param amount The amount whose asset determines which fee to look up. + * @return The transfer fee as a `Rate`, or `parityRate` if no fee is set. */ [[nodiscard]] Rate transferRate(ReadView const& view, STAmount const& amount); @@ -202,9 +516,42 @@ transferRate(ReadView const& view, STAmount const& amount); // //------------------------------------------------------------------------------ +/** Check whether a new holding object (trust line or MPToken) can be created. + * + * For IOU: verifies that the issuer's `AccountRoot` has `lsfDefaultRipple` + * set; returns `terNO_RIPPLE` if not, `terNO_ACCOUNT` if the issuer does not + * exist, `tesSUCCESS` for XRP. For MPT: delegates to the MPT-specific check. + * + * @note This function is read-only (takes `ReadView`) and is intended to be + * called during `preflight`. Any transactor that calls `addEmptyHolding` + * in `doApply` must call this function in `preflight` first. + * + * @param view Read-only ledger view. + * @param asset The asset for which a holding would be created. + * @return `tesSUCCESS` if a holding can be added; `terNO_RIPPLE`, + * `terNO_ACCOUNT`, or an MPT-specific error otherwise. + */ [[nodiscard]] TER canAddHolding(ReadView const& view, Asset const& asset); +/** Create an empty holding object (trust line or MPToken) for @p accountID. + * + * Dispatches to `addEmptyHolding(…, Issue)` or `addEmptyHolding(…, MPTIssue)` + * based on the runtime type of @p asset. The holding is created with zero + * balance and consumes an owner-count reserve slot. + * + * @note The caller must have invoked `canAddHolding` in `preflight` with the + * same view and asset to validate preconditions before calling this. + * + * @param view Mutable ledger view. + * @param accountID The account that will hold the asset. + * @param priorBalance The account's XRP balance before this transaction, + * used to test reserve sufficiency. + * @param asset The asset to create a holding for. + * @param journal Journal for trace/debug logging. + * @return `tesSUCCESS` on success, or a `tec`/`tef` error from the + * type-specific leaf. + */ [[nodiscard]] TER addEmptyHolding( ApplyView& view, @@ -213,6 +560,21 @@ addEmptyHolding( Asset const& asset, beast::Journal journal); +/** Delete a zero-balance holding object (trust line or MPToken) for @p accountID. + * + * Dispatches to `removeEmptyHolding(…, Issue)` or + * `removeEmptyHolding(…, MPTIssue)` based on the runtime type of @p asset. + * The holding must have a zero balance; a non-zero balance returns + * `tecHAS_OBLIGATIONS`. + * + * @param view Mutable ledger view. + * @param accountID The account whose holding should be removed. + * @param asset The asset identifying the holding to remove. + * @param journal Journal for trace/debug logging. + * @return `tesSUCCESS` on success; `tecHAS_OBLIGATIONS` if the balance is + * non-zero; `tecOBJECT_NOT_FOUND` if no holding exists; or a `tec`/`tef` + * error from the type-specific leaf. + */ [[nodiscard]] TER removeEmptyHolding( ApplyView& view, @@ -226,6 +588,25 @@ removeEmptyHolding( // //------------------------------------------------------------------------------ +/** Check whether @p account is authorized to hold or interact with @p asset. + * + * Dispatches to `requireAuth(…, Issue, …)` or `requireAuth(…, MPTIssue, …)` + * based on the runtime type of @p asset. + * + * - `StrongAuth`: verifies the holding object exists first; returns + * `tecNO_LINE` (IOU) or `tecNO_AUTH` (MPT) if absent. + * - `WeakAuth`: skips the existence check; returns success if authorization + * is not required even when no holding exists. + * - `Legacy`: maps to `StrongAuth` for MPT and `WeakAuth` for IOU to + * preserve historical behavior. + * + * @param view Read-only ledger view. + * @param asset The asset to check authorization for. + * @param account The account to check. + * @param authType Authorization strictness; defaults to `AuthType::Legacy`. + * @return `tesSUCCESS`, `tecNO_AUTH`, or `tecNO_LINE` depending on the asset + * type and authorization state. + */ [[nodiscard]] TER requireAuth( ReadView const& view, @@ -233,6 +614,20 @@ requireAuth( AccountID const& account, AuthType authType = AuthType::Legacy); +/** Check whether @p asset can be transferred from @p from to @p to. + * + * Dispatches to the IOU or MPT leaf. For IOU, checks rippling flags on the + * trustlines (returns `terNO_RIPPLE` if both sides block rippling). For MPT, + * checks `lsfMPTCanTransfer` on the issuance and the destination's + * authorization state. + * + * @param view Read-only ledger view. + * @param asset The asset to transfer. + * @param from The sending account. + * @param to The receiving account. + * @return `tesSUCCESS` if the transfer is permitted, or an asset-specific + * error (`terNO_RIPPLE`, `tecNO_AUTH`, etc.) otherwise. + */ [[nodiscard]] TER canTransfer(ReadView const& view, Asset const& asset, AccountID const& from, AccountID const& to); @@ -242,14 +637,29 @@ canTransfer(ReadView const& view, Asset const& asset, AccountID const& from, Acc // //------------------------------------------------------------------------------ -// Direct send w/o fees: -// - Redeeming IOUs and/or sending sender's own IOUs. -// - Create trust line of needed. -// --> bCheckIssuer : normally require issuer to be involved. -// [[nodiscard]] // nodiscard commented out so DirectStep.cpp compiles. - -/** Calls static directSendNoFeeIOU if saAmount represents Issue. - * Calls static directSendNoFeeMPT if saAmount represents MPTIssue. +/** Send @p saAmount directly without applying transfer fees or limit checks. + * + * Used for IOU redemption, intra-issuer transfers, and MPT moves where the + * issuer is one of the endpoints. Dispatches to `directSendNoFeeIOU` for + * IOU and `directSendNoFeeMPT` for MPT. + * + * For IOU, @p bCheckIssuer controls whether the function asserts that the + * issuer is one of the endpoints. For MPT, the issuer check is not performed + * (`bCheckIssuer` must be `false` for MPT). + * + * @note This function is intentionally **not** marked `[[nodiscard]]` for + * compatibility with `DirectStep.cpp`, which discards the return value in + * certain control paths. All other callers should inspect the result. + * + * @param view Mutable ledger view. + * @param uSenderID The sending account. + * @param uReceiverID The receiving account. + * @param saAmount The amount to send; its asset determines the dispatch. + * @param bCheckIssuer If `true` (IOU only), asserts that the issuer is one + * of the endpoints. Must be `false` for MPT. + * @param j Journal for trace/debug logging. + * @return `tesSUCCESS` on success, or a `tec`/`tef` error from the + * type-specific leaf. */ TER directSendNoFee( @@ -260,8 +670,30 @@ directSendNoFee( bool bCheckIssuer, beast::Journal j); -/** Calls static accountSendIOU if saAmount represents Issue. - * Calls static accountSendMPT if saAmount represents MPTIssue. +/** Send @p saAmount from @p from to @p to, applying transfer fees when + * applicable. + * + * This is the main asset-transfer entry point for transactors. Dispatches to + * `accountSendIOU` or `accountSendMPT` based on the asset type embedded in + * @p saAmount. Transfer fees are applied unless `WaiveTransferFee::Yes` is + * passed. + * + * The `allowOverflow` flag is forwarded to the MPT path only and controls + * whether `OutstandingAmount` may transiently exceed `MaximumAmount` during + * the two-phase issue-then-redeem structure used by the payment engine. Direct + * sends should use `AllowMPTOverflow::No`. + * + * @param view Mutable ledger view. + * @param from The sending account. + * @param to The receiving account. + * @param saAmount The amount to send. + * @param j Journal for trace/debug logging. + * @param waiveFee Whether to skip the transfer fee; defaults to `No`. + * @param allowOverflow Whether MPT OutstandingAmount may transiently exceed + * MaximumAmount; defaults to `No`. Use `Yes` only in payment-engine + * routing. + * @return `tesSUCCESS` on success, or a `tec`/`tef` error from the + * type-specific leaf. */ [[nodiscard]] TER accountSend( @@ -273,12 +705,34 @@ accountSend( WaiveTransferFee waiveFee = WaiveTransferFee::No, AllowMPTOverflow allowOverflow = AllowMPTOverflow::No); +/** A vector of (receiver, amount) pairs used by `accountSendMulti`. */ using MultiplePaymentDestinations = std::vector>; -/** Like accountSend, except one account is sending multiple payments (with the - * same asset!) simultaneously + +/** Send the same @p asset from @p senderID to multiple @p receivers in one + * atomic operation. * - * Calls static accountSendMultiIOU if saAmount represents Issue. - * Calls static accountSendMultiMPT if saAmount represents MPTIssue. + * Dispatches to `accountSendMultiIOU` or `accountSendMultiMPT` based on + * @p asset. Batching avoids repeated round-trips through the ledger state for + * the sender's balance and the issuance's `OutstandingAmount` field. + * + * For MPT, the `fixCleanup3_1_3` amendment switches the aggregate + * `MaximumAmount` check from a per-iteration stale-snapshot check (pre-fix) + * to an exact `uint64_t` running-total check (post-fix) to prevent precision + * loss at 19-digit magnitudes near `kMAX_MP_TOKEN_AMOUNT`. + * + * @note `receivers.size()` must be greater than 1 (asserted). + * + * @param view Mutable ledger view. + * @param senderID The account sending the asset. + * @param asset The asset to send (must match the type of all receiver + * amounts). + * @param receivers List of (AccountID, Number) destination pairs. All amounts + * must be non-negative. Sender-equals-receiver entries are silently + * skipped. + * @param j Journal for trace/debug logging. + * @param waiveFee Whether to skip transfer fees; defaults to `No`. + * @return `tesSUCCESS` on success, or a `tec`/`tef` error from the + * type-specific leaf. */ [[nodiscard]] TER accountSendMulti( @@ -289,6 +743,23 @@ accountSendMulti( beast::Journal j, WaiveTransferFee waiveFee = WaiveTransferFee::No); +/** Transfer XRP directly between two accounts without reserve or fee checks. + * + * XRP has no trust lines, no transfer fees, and no authorization model, so + * it bypasses the Asset-dispatch path entirely. Both @p from and @p to must + * be non-zero and distinct. Returns `telFAILED_PROCESSING` (open ledger) or + * `tecFAILED_PROCESSING` (closed ledger) if the sender's balance is + * insufficient. + * + * @param view Mutable ledger view. + * @param from The sending account; must not be `beast::kZERO`. + * @param to The receiving account; must not be `beast::kZERO`. + * @param amount The XRP amount to transfer; must be native (XRP). + * @param j Journal for trace/debug logging. + * @return `tesSUCCESS` on success; `telFAILED_PROCESSING` or + * `tecFAILED_PROCESSING` if balance is insufficient; `tefINTERNAL` if + * either account SLE cannot be found. + */ [[nodiscard]] TER transferXRP( ApplyView& view, diff --git a/include/xrpl/ledger/helpers/VaultHelpers.h b/include/xrpl/ledger/helpers/VaultHelpers.h index 14b0c004cb..a7cd6e84ed 100644 --- a/include/xrpl/ledger/helpers/VaultHelpers.h +++ b/include/xrpl/ledger/helpers/VaultHelpers.h @@ -1,3 +1,16 @@ +/** @file + * Pure arithmetic helpers for the XLS-65d Single-Sided Vault feature. + * + * Each function converts between the two token types a vault manages: + * the underlying *asset* (XRP, IOU, or MPT that depositors contribute) and + * vault *shares* (an MPT representing proportional ownership). Because MPT + * values are always integers every function makes an explicit rounding + * decision — and those decisions differ between the deposit and withdrawal + * paths to protect vault solvency. + * + * These functions are stateless and side-effect-free; all ledger mutations + * are the caller's responsibility. + */ #pragma once #include @@ -8,53 +21,105 @@ namespace xrpl { -/** From the perspective of a vault, return the number of shares to give - depositor when they offer a fixed amount of assets. Note, since shares are - MPT, this number is integral and always truncated in this calculation. - - @param vault The vault SLE. - @param issuance The MPTokenIssuance SLE for the vault's shares. - @param assets The amount of assets to convert. - - @return The number of shares, or nullopt on error. -*/ +/** Compute the shares minted when a depositor offers a fixed asset amount. + * + * Uses `sfAssetsTotal` from `vault` directly, *without* subtracting + * `sfLossUnrealized`. Unrealized losses are a risk borne by existing + * shareholders, not a discount for new depositors. + * + * **Bootstrap case**: when `sfAssetsTotal == 0` the result is + * `assets × 10^sfScale` (truncated), establishing the initial exchange rate. + * The non-bootstrap result is `(sfOutstandingAmount × assets) / sfAssetsTotal`, + * always truncated — depositors always receive a whole number of shares, never + * more than the assets strictly warrant. + * + * @note The deposit transactor calls this first, then back-calculates the + * true asset cost via `sharesToAssetsDeposit()` to ensure it never + * extracts more than the depositor offered. + * @throws std::overflow_error if `sfScale` is large enough to overflow + * XRPL's `Number` type; callers should catch and return `tecPATH_DRY`. + * + * @param vault The vault SLE; must contain `sfAsset`, `sfAssetsTotal`, + * `sfScale`, and `sfShareMPTID`. + * @param issuance The MPTokenIssuance SLE for the vault's share token; + * must contain `sfOutstandingAmount`. + * @param assets The asset amount to convert; must be non-negative and + * must match `vault->at(sfAsset)`. + * @return The integral share amount, or `nullopt` if `assets` is negative + * or its asset type does not match the vault. + */ [[nodiscard]] std::optional assetsToSharesDeposit( std::shared_ptr const& vault, std::shared_ptr const& issuance, STAmount const& assets); -/** From the perspective of a vault, return the number of assets to take from - depositor when they receive a fixed amount of shares. Note, since shares are - MPT, they are always an integral number. - - @param vault The vault SLE. - @param issuance The MPTokenIssuance SLE for the vault's shares. - @param shares The amount of shares to convert. - - @return The number of assets, or nullopt on error. -*/ +/** Compute the asset cost for a depositor who will receive a fixed share amount. + * + * This is the inverse of `assetsToSharesDeposit()` and is used in the second + * step of the deposit calculation: after truncating the forward direction to + * determine how many whole shares are created, the transactor calls this + * function to derive the exact asset amount to collect. + * + * Uses `sfAssetsTotal` directly, without subtracting `sfLossUnrealized`, + * matching the deposit-path convention. + * + * **Bootstrap case**: when `sfAssetsTotal == 0` the result uses `sfScale` to + * reverse the bootstrap formula applied by `assetsToSharesDeposit()`. + * + * @throws std::overflow_error if `sfScale` is large enough to overflow + * XRPL's `Number` type; callers should catch and return `tecPATH_DRY`. + * + * @param vault The vault SLE. + * @param issuance The MPTokenIssuance SLE for the vault's share token. + * @param shares The share amount to convert; must be non-negative and must + * match `vault->at(sfShareMPTID)`. + * @return The asset amount, or `nullopt` if `shares` is negative or its + * asset type does not match the vault's share MPT. + */ [[nodiscard]] std::optional sharesToAssetsDeposit( std::shared_ptr const& vault, std::shared_ptr const& issuance, STAmount const& shares); -/** Controls whether to truncate shares instead of rounding. */ +/** Controls whether to truncate (floor) the share result instead of rounding. + * + * `No` (the default) rounds to nearest, ensuring the vault is never + * shortchanged when computing shares to redeem for a fixed asset withdrawal. + * `Yes` applies floor truncation, used when the caller explicitly needs + * conservative (depositor-favoring) rounding. + */ enum class TruncateShares : bool { No = false, Yes = true }; -/** From the perspective of a vault, return the number of shares to demand from - the depositor when they ask to withdraw a fixed amount of assets. Since - shares are MPT this number is integral, and it will be rounded to nearest - unless explicitly requested to be truncated instead. - - @param vault The vault SLE. - @param issuance The MPTokenIssuance SLE for the vault's shares. - @param assets The amount of assets to convert. - @param truncate Whether to truncate instead of rounding. - - @return The number of shares, or nullopt on error. -*/ +/** Compute the shares a withdrawer must redeem to receive a fixed asset amount. + * + * Unlike the deposit path, this function subtracts `sfLossUnrealized` from + * `sfAssetsTotal` before computing the exchange rate. Withdrawers receive fewer + * assets per share when the vault has recorded unrealized losses, preventing + * early withdrawers from exiting at inflated prices at the expense of remaining + * holders. + * + * The result is rounded to nearest by default (`TruncateShares::No`), ensuring + * the vault is not shortchanged. The withdraw transactor then back-calculates + * the actual assets delivered via `sharesToAssetsWithdraw()` for a precise + * two-step computation. + * + * If `sfAssetsTotal - sfLossUnrealized == 0` (fully insolvent vault), returns + * a zero-valued `STAmount` rather than dividing by zero. + * + * @throws std::overflow_error if arithmetic overflows XRPL's `Number` type; + * callers should catch and return `tecPATH_DRY`. + * + * @param vault The vault SLE; must contain `sfAsset`, `sfAssetsTotal`, + * `sfLossUnrealized`, and `sfShareMPTID`. + * @param issuance The MPTokenIssuance SLE for the vault's share token. + * @param assets The asset amount to convert; must be non-negative and must + * match `vault->at(sfAsset)`. + * @param truncate Whether to truncate instead of rounding to nearest. + * @return The integral share amount, or `nullopt` if `assets` is negative or + * its asset type does not match the vault. + */ [[nodiscard]] std::optional assetsToSharesWithdraw( std::shared_ptr const& vault, @@ -62,16 +127,25 @@ assetsToSharesWithdraw( STAmount const& assets, TruncateShares truncate = TruncateShares::No); -/** From the perspective of a vault, return the number of assets to give the - depositor when they redeem a fixed amount of shares. Note, since shares are - MPT, they are always an integral number. - - @param vault The vault SLE. - @param issuance The MPTokenIssuance SLE for the vault's shares. - @param shares The amount of shares to convert. - - @return The number of assets, or nullopt on error. -*/ +/** Compute the assets delivered when a withdrawer redeems a fixed share amount. + * + * Like `assetsToSharesWithdraw()`, this function subtracts `sfLossUnrealized` + * from `sfAssetsTotal` before computing the exchange rate, so withdrawers + * bear their proportional share of any recorded losses. + * + * If `sfAssetsTotal - sfLossUnrealized == 0` (fully insolvent vault), returns + * a zero-valued `STAmount` rather than dividing by zero. + * + * @throws std::overflow_error if arithmetic overflows XRPL's `Number` type; + * callers should catch and return `tecPATH_DRY`. + * + * @param vault The vault SLE. + * @param issuance The MPTokenIssuance SLE for the vault's share token. + * @param shares The share amount to convert; must be non-negative and must + * match `vault->at(sfShareMPTID)`. + * @return The asset amount, or `nullopt` if `shares` is negative or its + * asset type does not match the vault's share MPT. + */ [[nodiscard]] std::optional sharesToAssetsWithdraw( std::shared_ptr const& vault, diff --git a/include/xrpl/nodestore/Backend.h b/include/xrpl/nodestore/Backend.h index 124cd99db5..6cc34356f0 100644 --- a/include/xrpl/nodestore/Backend.h +++ b/include/xrpl/nodestore/Backend.h @@ -6,33 +6,50 @@ namespace xrpl::NodeStore { -/** A backend used for the NodeStore. - - The NodeStore uses a swappable backend so that other database systems - can be tried. Different databases may offer various features such - as improved performance, fault tolerant or distributed storage, or - all in-memory operation. - - A given instance of a backend is fixed to a particular key size. -*/ +/** Pure abstract storage interface for the NodeStore persistence layer. + * + * Every ledger object (account states, transactions, ledger headers) is a + * `NodeObject` keyed by its 256-bit hash. `Backend` defines the narrow + * interface that lets the `Database` layer remain independent of the + * underlying engine — NuDB, RocksDB, or an in-memory store for tests all + * satisfy this contract identically. + * + * A backend instance is fixed to a particular key size (always 32 bytes in + * practice, matching `NodeObject::keyBytes`) at construction. + * + * **Concurrency contract**: `fetch()` and `store()` will be called + * concurrently by multiple threads; implementations must be internally + * thread-safe for these two operations. `storeBatch()` and `forEach()` are + * never called concurrently with each other or with other writes. + * + * **Lifecycle**: Construction is separated from initialization via `open()`. + * Backends are never constructed directly — use `Factory::createInstance()` + * dispatched through `Manager`. + * + * @see Factory, Manager, Database + */ class Backend { public: /** Destroy the backend. - - All open files are closed and flushed. If there are batched writes - or other tasks scheduled, they will be completed before this call - returns. - */ + * + * All open files are closed and flushed. Any batched writes or scheduled + * tasks complete before this returns, so dropping a `unique_ptr` + * cannot silently discard data. + */ virtual ~Backend() = default; - /** Get the human-readable name of this backend. - This is used for diagnostic output. - */ + /** Return the human-readable name of this backend, used in diagnostics. */ virtual std::string getName() = 0; - /** Get the block size for backends that support it + /** Return the storage block size, if the backend has a meaningful one. + * + * NuDB organizes data into fixed-size blocks; callers that care about + * I/O alignment or prefetch granularity can query this without + * downcasting. Backends with no block concept return `std::nullopt`. + * + * @return Block size in bytes, or `std::nullopt` if not applicable. */ [[nodiscard]] virtual std::optional getBlockSize() const @@ -40,25 +57,37 @@ public: return std::nullopt; } - /** Open the backend. - @param createIfMissing Create the database files if necessary. - This allows the caller to catch exceptions. - */ + /** Open the backend, optionally creating the database if absent. + * + * Separating `open()` from the constructor allows I/O errors to be + * caught without wrapping constructors in try/catch. + * + * @param createIfMissing If `true`, create the database files when they + * do not exist. Pass `false` to fail fast on a missing database. + * @throws implementation-defined exception on I/O or database errors. + */ virtual void open(bool createIfMissing = true) = 0; - /** Returns true is the database is open. - */ + /** Return `true` if the backend is currently open. */ virtual bool isOpen() = 0; - /** Open the backend. - @param createIfMissing Create the database files if necessary. - @param appType Deterministic appType used to create a backend. - @param uid Deterministic uid used to create a backend. - @param salt Deterministic salt used to create a backend. - @throws std::runtime_error is function is called not for NuDB backend. - */ + /** Open the backend with deterministic NuDB header parameters. + * + * This overload exists exclusively to support NuDB's header-level + * application identification (appnum, uid, salt). It enables shard + * databases to be created with reproducible identifiers. + * + * @param createIfMissing Create the database files if they do not exist. + * @param appType Application-defined type tag embedded in the NuDB header. + * @param uid Deterministic unique identifier for this database instance. + * @param salt Deterministic salt value used during NuDB database creation. + * @throws std::runtime_error for every backend except NuDB, as this + * capability is not part of the general interface. + * @note Non-NuDB backends inherit a default implementation that always + * throws, clearly advertising that the capability is unavailable. + */ virtual void open(bool createIfMissing, uint64_t appType, uint64_t uid, uint64_t salt) { @@ -66,75 +95,137 @@ public: "Deterministic appType/uid/salt not supported by backend " + getName()); } - /** Close the backend. - This allows the caller to catch exceptions. - */ + /** Close the backend, flushing any pending writes. + * + * Separating `close()` from the destructor allows the caller to catch + * and handle I/O exceptions explicitly. + */ virtual void close() = 0; - /** Fetch a single object. - If the object is not found or an error is encountered, the - result will indicate the condition. - @note This will be called concurrently. - @param hash The hash of the object. - @param pObject [out] The created object if successful. - @return The result of the operation. - */ + /** Fetch a single object by its 256-bit hash. + * + * On success, `*pObject` is set to the retrieved `NodeObject`. On any + * non-`Ok` outcome, `*pObject` is left unchanged (or reset). + * + * @note Called concurrently by multiple threads; implementations must + * be thread-safe for this operation. + * @param hash The 256-bit hash key identifying the object. + * @param pObject Output parameter; receives the fetched object on success. + * @return `Status::Ok` on success, `Status::NotFound` if the key is + * absent, `Status::DataCorrupt` if the stored blob fails validation, + * or another `Status` value on backend or unknown errors. + */ virtual Status fetch(uint256 const& hash, std::shared_ptr* pObject) = 0; - /** Fetch a batch synchronously. */ + /** Fetch a batch of objects by their 256-bit hashes. + * + * Amortizes round-trip or I/O overhead when prefetching sets of related + * objects. The returned vector is parallel to `hashes`: a null + * `shared_ptr` at position `i` indicates the object at `hashes[i]` was + * not found or could not be retrieved. + * + * @param hashes Ordered list of 256-bit hash keys to fetch. + * @return A pair of (results vector, aggregate Status). Each element in + * the results vector is the fetched object, or an empty + * `shared_ptr` if the corresponding hash was not found. + */ virtual std::pair>, Status> fetchBatch(std::vector const& hashes) = 0; /** Store a single object. - Depending on the implementation this may happen immediately - or deferred using a scheduled task. - @note This will be called concurrently. - @param object The object to store. - */ + * + * Depending on the implementation, the write may be synchronous or + * deferred to a scheduled task (e.g., via `BatchWriter`). Either way, + * the object is guaranteed to be durable before the backend is destroyed. + * + * @note Called concurrently by multiple threads; implementations must + * be thread-safe for this operation. + * @param object The `NodeObject` to persist. + */ virtual void store(std::shared_ptr const& object) = 0; - /** Store a group of objects. - @note This function will not be called concurrently with - itself or @ref store. - */ + /** Store a group of objects as a batch. + * + * More efficient than repeated `store()` calls for backends that + * support atomic or coalesced multi-key writes (e.g., RocksDB + * `WriteBatch`). The entire batch is treated as a single unit. + * + * @note Never called concurrently with itself or with `store()`. + * @param batch The collection of `NodeObject`s to persist. + */ virtual void storeBatch(Batch const& batch) = 0; + /** Flush all previously submitted stores to durable storage. + * + * Provides an explicit durability barrier: after `sync()` returns, + * all objects passed to `store()` or `storeBatch()` before the call + * are guaranteed to be on disk. Backends backed by a write-ahead log + * (e.g., RocksDB) may implement this as a no-op. + */ virtual void sync() = 0; - /** Visit every object in the database - This is usually called during import. - @note This routine will not be called concurrently with itself - or other methods. - @see import - */ + /** Invoke a callback for every object stored in the backend. + * + * Typically used during database import or migration. Because it closes + * and reopens the underlying database (NuDB), it must not be called + * while concurrent reads or writes are in flight. + * + * @note Never called concurrently with itself or with any other method. + * @param f Callback invoked once per stored object; receives a + * `shared_ptr` for each entry in the database. + * @see importInternal + */ virtual void forEach(std::function)> f) = 0; - /** Estimate the number of write operations pending. */ + /** Return an estimate of the number of pending write operations. + * + * Used by the `Database` layer for back-pressure and diagnostic + * reporting. The value is advisory; implementations may return 0 if + * writes are always synchronous (e.g., NuDB). + * + * @return Approximate count of writes not yet flushed to storage. + */ virtual int getWriteLoad() = 0; - /** Remove contents on disk upon destruction. */ + /** Schedule the backend's on-disk files for deletion on destruction. + * + * After this call, the next `close()` (including the one in the + * destructor) removes all database files from the filesystem. Used by + * temporary databases — unit tests and ephemeral shard stores — that + * require automatic cleanup without external management. + */ virtual void setDeletePath() = 0; - /** Perform consistency checks on database. + /** Perform an offline consistency check of the stored data. * - * This method is implemented only by NuDBBackend. It is not yet called - * anywhere, but it might be a good idea to one day call it at startup to - * avert a crash. + * Closes and reopens the database around the check, so it must not be + * called while I/O is in progress. Currently implemented only by + * `NuDBBackend`; all other backends inherit a no-op. + * + * @note Not yet called at startup, but could one day be invoked at + * launch to detect on-disk corruption before it causes a crash. */ virtual void verify() { } - /** Returns the number of file descriptors the backend expects to need. */ + /** Return the number of file descriptors this backend expects to consume. + * + * The `Database` base class aggregates these values across all open + * backends and exposes the total so the process can pre-check against + * the OS file descriptor limit before opening any databases. + * + * @return Expected file descriptor count (e.g., 3 for NuDB, 0 for Null). + */ [[nodiscard]] virtual int fdRequired() const = 0; }; diff --git a/include/xrpl/nodestore/Database.h b/include/xrpl/nodestore/Database.h index ca2dde560c..914a8399ab 100644 --- a/include/xrpl/nodestore/Database.h +++ b/include/xrpl/nodestore/Database.h @@ -1,3 +1,14 @@ +/** @file + * Abstract base class for the NodeStore persistence layer. + * + * Defines the full public contract for node object storage: async and + * synchronous fetch, store, import, and diagnostics. Concrete subclasses + * (`DatabaseNodeImp`, `DatabaseRotatingImp`) implement the private virtual + * `fetchNodeObject()` and `forEach()` hooks; all instrumentation (timing, + * counters, scheduler callbacks) is applied in this base class and cannot + * be bypassed. + */ + #pragma once #include @@ -12,100 +23,159 @@ namespace xrpl::NodeStore { -/** Persistency layer for NodeObject - - A Node is a ledger object which is uniquely identified by a key, which is - the 256-bit hash of the body of the node. The payload is a variable length - block of serialized data. - - All ledger data is stored as node objects and as such, needs to be persisted - between launches. Furthermore, since the set of node objects will in - general be larger than the amount of available memory, purged node objects - which are later accessed must be retrieved from the node store. - - @see NodeObject -*/ +/** Persistence layer for NodeObject records. + * + * Every ledger datum — account states, transactions, ledger headers — is + * stored as a `NodeObject` keyed by the 256-bit hash of its payload. Because + * the total object set typically exceeds available memory, any hash absent + * from the in-memory cache must be fetched from disk through this class. + * + * `Database` owns the async read thread pool and all performance counters. + * The public non-virtual `fetchNodeObject()` wraps the private pure-virtual + * one, applying timing, hit/miss accounting, and `Scheduler::onFetch()` + * callbacks — so no subclass can escape the instrumentation. + * + * **Shutdown ordering**: Derived classes **must** call `stop()` in their own + * destructors before the base destructor runs. Worker threads invoke the + * virtual `fetchNodeObject()` through a subclass vtable; if the derived + * object is destroyed before all threads have exited, a waking thread will + * call through a dangling vtable entry (undefined behaviour). The base + * destructor calls `stop()` only as a last-resort safety net. + * + * @see NodeObject, Backend, Scheduler, DatabaseNodeImp, DatabaseRotatingImp + */ class Database { public: Database() = delete; - /** Construct the node store. - - @param scheduler The scheduler to use for performing asynchronous tasks. - @param readThreads The number of asynchronous read threads to create. - @param config The configuration settings - @param journal Destination for logging output. - */ + /** Construct the node store and start the async read thread pool. + * + * Validates configuration parameters, then spawns `readThreads` detached + * worker threads. Threads are controlled by `readStopping_`; `stop()` + * spin-waits (≤ 30 s) until `readThreads_` reaches zero. + * + * @param scheduler Task scheduler for async I/O dispatch and telemetry + * callbacks; must outlive this object. + * @param readThreads Number of prefetch worker threads to create; clamped + * to at least 1. + * @param config `[node_db]` config section; reads `earliest_seq` (default + * `kXRP_LEDGER_EARLIEST_SEQ`, must be ≥ 1) and `rq_bundle` (default 4, + * clamped [1, 64]). + * @param j Logging sink. + * @throws std::runtime_error if `earliest_seq` < 1 or `rq_bundle` is + * outside [1, 64]. + */ Database(Scheduler& scheduler, int readThreads, Section const& config, beast::Journal j); /** Destroy the node store. - All pending operations are completed, pending writes flushed, - and files closed before this returns. - */ + * + * Calls `stop()` as a safety net to drain the read queue and wait for all + * worker threads to exit. Derived classes **must** call `stop()` in their + * own destructors first — worker threads invoke the pure-virtual + * `fetchNodeObject()` through the subclass vtable, which is already gone + * by the time this base destructor runs. + */ virtual ~Database(); - /** Retrieve the name associated with this backend. - This is used for diagnostics and may not reflect the actual path - or paths used by the underlying backend. - */ + /** Return the name of the underlying backend for diagnostics. + * + * The returned string may not reflect the actual on-disk path when + * multiple backends are in use (e.g. `DatabaseRotatingImp`). + * + * @return A human-readable backend identifier. + */ virtual std::string getName() const = 0; - /** Import objects from another database. */ + /** Bulk-import all objects from another database into this one. + * + * Iterates every `NodeObject` in @p source and writes it to this + * database's backend. Implementations typically delegate to + * `importInternal()`. Large databases may take significant time. + * + * @param source The source database to read from; must remain valid + * and quiescent (no concurrent writes) for the duration of the call. + */ virtual void importDatabase(Database& source) = 0; - /** Retrieve the estimated number of pending write operations. - This is used for diagnostics. - */ + /** Return the estimated number of pending write operations. + * + * Used for backpressure diagnostics; the value is approximate and may + * change immediately after it is read. + * + * @return Pending write count, or 0 if the backend does not batch writes. + */ virtual std::int32_t getWriteLoad() const = 0; - /** Store the object. - - The caller's Blob parameter is overwritten. - - @param type The type of object. - @param data The payload of the object. The caller's - variable is overwritten. - @param hash The 256-bit hash of the payload data. - @param ledgerSeq The sequence of the ledger the object belongs to. - - @return `true` if the object was stored? - */ + /** Persist a node object to the backend. + * + * Takes ownership of @p data (the caller's `Blob` is consumed). The object + * is keyed by @p hash; backends are content-addressed, so storing an object + * whose hash already exists is a no-op (same key → same data). + * + * @param type The semantic type of the object (ledger, account node, etc.). + * @param data Serialized payload; moved into the backend — caller's variable + * is left in a valid but unspecified state. + * @param hash 256-bit hash of @p data. The caller is responsible for + * correctness; the hash is not re-verified by the store. + * @param ledgerSeq The ledger sequence this object belongs to; used by + * rotating backends to route writes to the correct physical file. + */ virtual void store(NodeObjectType type, Blob&& data, uint256 const& hash, std::uint32_t ledgerSeq) = 0; - /* Check if two ledgers are in the same database - - If these two sequence numbers map to the same database, - the result of a fetch with either sequence number would - be identical. - - @param s1 The first sequence number - @param s2 The second sequence number - - @return 'true' if both ledgers would be in the same DB - - */ + /** Return whether two ledger sequence numbers resolve to the same backend. + * + * When this returns `true`, a fetch with either sequence number will + * reach the same physical storage and yield identical results. The async + * thread pool uses this to avoid redundant backend reads when multiple + * callbacks for the same hash were registered with different sequence + * numbers. + * + * `DatabaseNodeImp` always returns `true` (single backend). + * `DatabaseRotatingImp` returns `false` when the sequences straddle a + * rotation boundary. + * + * @param s1 First ledger sequence number. + * @param s2 Second ledger sequence number. + * @return `true` if both sequences map to the same physical backend. + */ virtual bool isSameDB(std::uint32_t s1, std::uint32_t s2) = 0; + /** Flush any buffered writes to durable storage. + * + * Called by maintenance paths (e.g. ledger close) to ensure consistency. + * Not latency-sensitive; implementations may hold locks for the full call. + */ virtual void sync() = 0; - /** Fetch a node object. - If the object is known to be not in the database, isn't found in the - database during the fetch, or failed to load correctly during the fetch, - `nullptr` is returned. - - @note This can be called concurrently. - @param hash The key of the object to retrieve. - @param ledgerSeq The sequence of the ledger where the object is stored. - @param fetchType the type of fetch, synchronous or asynchronous. - @return The object, or nullptr if it couldn't be retrieved. - */ + /** Fetch a node object by hash, recording timing and hit/miss metrics. + * + * This is the public entry point for all node lookups. It wraps the + * private pure-virtual `fetchNodeObject(hash, seq, FetchReport&, duplicate)` + * using the Template Method pattern: timing, atomic counters, and + * `Scheduler::onFetch()` are applied here and cannot be bypassed by + * subclasses. + * + * Returns `nullptr` if the object is absent, could not be decoded, or the + * backend encountered an error. + * + * @note Thread-safe; may be called concurrently from any thread. + * @param hash 256-bit content hash of the desired object. + * @param ledgerSeq Ledger sequence that owns this object; used by rotating + * backends to select the correct physical file. Defaults to 0. + * @param fetchType `FetchType::Synchronous` (default) or + * `FetchType::Async` when called from the async worker pool. + * @param duplicate When `true`, the object is also written into the + * writable backend after being found in the archive backend + * (`DatabaseRotatingImp` promotion path). Defaults to `false`. + * @return The requested `NodeObject`, or `nullptr` on miss or error. + */ std::shared_ptr fetchNodeObject( uint256 const& hash, @@ -113,75 +183,124 @@ public: FetchType fetchType = FetchType::Synchronous, bool duplicate = false); - /** Fetch an object without waiting. - If I/O is required to determine whether or not the object is present, - `false` is returned. Otherwise, `true` is returned and `object` is set - to refer to the object, or `nullptr` if the object is not present. - If I/O is required, the I/O is scheduled and `true` is returned - - @note This can be called concurrently. - @param hash The key of the object to retrieve - @param ledgerSeq The sequence of the ledger where the - object is stored. - @param callback Callback function when read completes - */ + /** Schedule a non-blocking background fetch for a node object. + * + * Enqueues a `(hash, ledgerSeq, callback)` entry in the async read map. + * Multiple calls for the same hash are coalesced: a single backend read + * satisfies all registered callbacks. If `isStopping()` is `true` at the + * time of the call, the request is silently discarded and the callback + * will never fire. + * + * @note Thread-safe; may be called concurrently from any thread. + * @param hash 256-bit content hash of the desired object. + * @param ledgerSeq Ledger sequence that owns this object; passed through + * to `isSameDB()` for multi-sequence coalescing. + * @param callback Invoked on a worker thread with the fetched + * `NodeObject`, or `nullptr` on miss or error. + */ virtual void asyncFetch( uint256 const& hash, std::uint32_t ledgerSeq, std::function const&)>&& callback); - /** Gather statistics pertaining to read and write activities. - * - * @param obj Json object reference into which to place counters. - */ + // --- Performance counters (all lock-free atomic reads) --- + + /** Return the total number of objects written since construction. */ std::uint64_t getStoreCount() const { return storeCount_; } + /** Return the total number of fetch attempts (hits + misses). */ std::uint32_t getFetchTotalCount() const { return fetchTotalCount_; } + /** Return the number of fetch attempts that found the requested object. */ std::uint32_t getFetchHitCount() const { return fetchHitCount_; } + /** Return the cumulative byte count of all stored objects. */ std::uint64_t getStoreSize() const { return storeSz_; } + /** Return the cumulative byte count of all successfully fetched objects. */ std::uint32_t getFetchSize() const { return fetchSz_; } + /** Populate a JSON object with read/write diagnostics for `get_counts` RPC. + * + * Snapshots the async read queue depth (under `readLock_`) and then reads + * thread counts, request bundle size, and all atomic counters without + * holding any lock. The resulting fields include: `read_queue`, + * `read_threads_total`, `read_threads_running`, `read_request_bundle`, + * `node_writes`, `node_reads_total`, `node_reads_hit`, + * `node_written_bytes`, `node_read_bytes`, `node_reads_duration_us`. + * + * @param obj A JSON object to populate; must satisfy `obj.isObject()`. + */ void getCountsJson(json::Value& obj); - /** Returns the number of file descriptors the database expects to need */ + /** Return the number of file descriptors this database expects to hold open. + * + * Aggregated from the underlying backend(s). Used by the application to + * check that the process file-descriptor limit is sufficient before + * opening backends. Inaccurate values cause silent failures when the + * limit is exceeded. + * + * @return File descriptor count, or 0 if not set by the subclass. + */ int fdRequired() const { return fdRequired_; } + /** Begin orderly shutdown of the async read thread pool. + * + * Sets `readStopping_`, clears the pending `read_` queue, broadcasts on + * `readCondVar_`, then spin-yields until `readThreads_` reaches zero. + * An assertion fires if shutdown takes longer than 30 seconds. + * + * Idempotent: a second call after shutdown has already completed is a + * no-op. Derived classes must call this in their own destructors before + * their data members are torn down. + */ virtual void stop(); + /** Return whether `stop()` has been called. + * + * Uses a relaxed atomic load — only the flag value is observed; no + * ordering is imposed on surrounding operations. + * + * @return `true` once `stop()` has been invoked. + */ bool isStopping() const; - /** @return The earliest ledger sequence allowed + /** Return the earliest ledger sequence this database will serve. + * + * Configured via `earliest_seq` in `[node_db]`; defaults to + * `kXRP_LEDGER_EARLIEST_SEQ` (32570 on the main network). The value is + * constant after construction. Only unit tests or alternate networks + * should set this below the default. + * + * @return The minimum valid ledger sequence number, always ≥ 1. */ [[nodiscard]] std::uint32_t earliestLedgerSeq() const noexcept @@ -190,26 +309,34 @@ public: } protected: - beast::Journal const j_; - Scheduler& scheduler_; + beast::Journal const j_; ///< Logging sink; set at construction. + Scheduler& scheduler_; ///< Task scheduler for async dispatch and telemetry. + + /** Number of file descriptors consumed by the underlying backend(s). + * Subclasses set this in their constructors; read by `fdRequired()`. + */ int fdRequired_{0}; - std::atomic fetchHitCount_{0}; - std::atomic fetchSz_{0}; + std::atomic fetchHitCount_{0}; ///< Fetches that returned a non-null object. + std::atomic fetchSz_{0}; ///< Cumulative bytes returned by successful fetches. - // The default is XRP_LEDGER_EARLIEST_SEQ (32570) to match the XRP ledger - // network's earliest allowed ledger sequence. Can be set through the - // configuration file using the 'earliest_seq' field under the 'node_db' - // stanza. If specified, the value must be greater than zero. - // Only unit tests or alternate - // networks should change this value. + /** Minimum ledger sequence this store will serve; constant after construction. + * Defaults to `kXRP_LEDGER_EARLIEST_SEQ` (32570). Must be ≥ 1. + */ std::uint32_t const earliestLedgerSeq_; - // The maximum number of requests a thread extracts from the queue in an - // attempt to minimize the overhead of mutex acquisition. This is an - // advanced tunable, via the config file. The default value is 4. + /** Maximum number of read-queue entries extracted per mutex acquisition. + * Amortises lock overhead under load. Configured via `rq_bundle` in + * `[node_db]`; clamped to [1, 64]; defaults to 4. + */ int const requestBundle_; + /** Update store counters after a successful batch write. + * + * @param count Number of objects written. + * @param sz Total byte size of those objects. + * @note Asserts `count <= sz` — byte total must be ≥ item count. + */ void storeStats(std::uint64_t count, std::uint64_t sz) { @@ -218,10 +345,32 @@ protected: storeSz_ += sz; } - // Called by the public import function + /** Bulk-import all objects from @p srcDB into @p dstBackend. + * + * Iterates @p srcDB via `forEach()`, accumulates objects into batches of + * `kBATCH_WRITE_PREALLOCATION_SIZE`, and flushes each batch with + * `dstBackend.storeBatch()`. Byte statistics are recorded via + * `storeStats()` after each flush. On exception, logs the error and + * returns early without aborting the overall import. + * + * Called by subclass `importDatabase()` implementations. + * + * @param dstBackend Destination backend; must be open and writable. + * @param srcDB Source database; iterated sequentially — no concurrent + * writes to @p srcDB should occur during the call. + */ void importInternal(Backend& dstBackend, Database& srcDB); + /** Merge externally-collected fetch metrics into the atomic counters. + * + * Used by subclasses that perform their own batched reads (e.g. import + * paths) and need to credit the counters in bulk rather than per-object. + * + * @param fetches Number of fetch attempts to add to `fetchTotalCount_`. + * @param hits Number of successful fetches to add to `fetchHitCount_`. + * @param duration Elapsed microseconds to add to `fetchDurationUs_`. + */ void updateFetchMetrics(uint64_t fetches, uint64_t hits, uint64_t duration) { @@ -231,26 +380,51 @@ protected: } private: - std::atomic storeCount_{0}; - std::atomic storeSz_{0}; - std::atomic fetchTotalCount_{0}; - std::atomic fetchDurationUs_{0}; - std::atomic storeDurationUs_{0}; + // --- Write-side atomic counters --- + std::atomic storeCount_{0}; ///< Total objects stored. + std::atomic storeSz_{0}; ///< Total bytes stored. + std::atomic storeDurationUs_{0}; ///< Cumulative store duration (µs); reserved. - mutable std::mutex readLock_; - std::condition_variable readCondVar_; + // --- Fetch-side atomic counters (incremented by the public fetchNodeObject wrapper) --- + std::atomic fetchTotalCount_{0}; ///< Total fetch attempts. + std::atomic fetchDurationUs_{0}; ///< Cumulative fetch duration (µs). - // reads to do + // --- Async read-queue state (all guarded by readLock_ except atomic members) --- + mutable std::mutex readLock_; ///< Guards `read_` and `readCondVar_`. + std::condition_variable readCondVar_; ///< Wakes worker threads when `read_` is non-empty or stopping. + + /** Pending async read requests, keyed by hash. + * + * Each map entry holds all `(ledgerSeq, callback)` pairs registered for a + * given hash. Multiple calls to `asyncFetch()` with the same hash are + * coalesced here so that a single backend read services all callbacks. + */ std::map< uint256, std::vector< std::pair const&)>>>> read_; - std::atomic readStopping_ = false; - std::atomic readThreads_ = 0; - std::atomic runningThreads_ = 0; + std::atomic readStopping_ = false; ///< Set by `stop()`; workers exit when observed. + std::atomic readThreads_ = 0; ///< Count of live worker threads; reaches 0 on full stop. + std::atomic runningThreads_ = 0; ///< Threads currently active (not blocked on condvar). + /** Backend fetch hook — the Template Method target. + * + * Called exclusively by the public non-virtual `fetchNodeObject()` wrapper, + * which applies timing and metrics around this call. Subclasses must + * implement this and may not call the public wrapper from within it. + * + * @param hash 256-bit content hash to look up. + * @param ledgerSeq Ledger sequence, used by rotating backends to select + * the correct physical file. + * @param fetchReport Mutable report populated by the implementation; + * the public wrapper reads `fetchReport.wasFound` and `elapsed`. + * @param duplicate When `true`, if the object is found in the archive + * backend it should also be written back to the writable backend + * (promotion path for `DatabaseRotatingImp`). + * @return The fetched `NodeObject`, or `nullptr` on miss or error. + */ virtual std::shared_ptr fetchNodeObject( uint256 const& hash, @@ -258,16 +432,26 @@ private: FetchReport& fetchReport, bool duplicate) = 0; - /** Visit every object in the database - This is usually called during import. - - @note This routine will not be called concurrently with itself - or other methods. - @see import - */ + /** Iterate every object in the database and invoke @p f for each one. + * + * Used exclusively by `importInternal()`. Implementations may close and + * reopen the underlying store (e.g. NuDB) and are not safe for concurrent + * access; the caller must ensure no other reads or writes occur during + * iteration. + * + * @note Never called concurrently with itself or other methods. + * @param f Callback invoked with each `NodeObject`; must not be null. + */ virtual void forEach(std::function)> f) = 0; + /** Worker thread body for the async read pool. + * + * Loops waiting on `readCondVar_`, extracts up to `requestBundle_` entries + * from `read_` per lock acquisition, and dispatches each to the private + * `fetchNodeObject()`. Handles multi-sequence coalescing via `isSameDB()`. + * Exits when `readStopping_` is observed, then decrements `readThreads_`. + */ void threadEntry(); }; diff --git a/include/xrpl/nodestore/DatabaseRotating.h b/include/xrpl/nodestore/DatabaseRotating.h index a7deed294a..cb8d63ffc1 100644 --- a/include/xrpl/nodestore/DatabaseRotating.h +++ b/include/xrpl/nodestore/DatabaseRotating.h @@ -1,17 +1,47 @@ +/** @file + * Abstract interface extending `Database` with a two-backend rotation + * operation for online ledger history deletion. + */ + #pragma once #include namespace xrpl::NodeStore { -/* This class has two key-value store Backend objects for persisting SHAMap - * records. This facilitates online deletion of data. New backends are - * rotated in. Old ones are rotated out and deleted. +/** Abstract seam for the two-backend rotation scheme that enables online + * deletion of ledger history without taking the node offline. + * + * The concrete subclass `DatabaseRotatingImp` maintains two physical + * `Backend` objects: a *writable* backend that receives all current writes + * and an *archive* backend holding older data. When enough new history has + * accumulated, `SHAMapStoreImp` calls `rotate()` to atomically promote the + * writable backend to the archive role, install a fresh writable backend, + * and discard the old archive — all without interrupting read or write + * traffic. + * + * `DatabaseRotating` carries no state; it extends `Database` solely with + * the `rotate()` pure-virtual method. Components that drive rotation + * (currently only `SHAMapStoreImp`) hold a `DatabaseRotating*` pointer, + * keeping the rotation mechanism decoupled from storage format. + * + * @see DatabaseRotatingImp, Database, SHAMapStoreImp */ - class DatabaseRotating : public Database { public: + /** Construct the rotating database and start the async read thread pool. + * + * Delegates entirely to `Database(scheduler, readThreads, config, + * journal)`. The two physical backends are supplied when constructing + * the concrete `DatabaseRotatingImp` subclass. + * + * @param scheduler Task scheduler for async I/O dispatch and telemetry; + * must outlive this object. + * @param readThreads Number of prefetch worker threads to create. + * @param config `[node_db]` config section forwarded to `Database`. + * @param journal Logging sink. + */ DatabaseRotating( Scheduler& scheduler, int readThreads, @@ -21,13 +51,37 @@ public: { } - /** Rotates the backends. - - @param newBackend New writable backend - @param f A function executed after the rotation outside of lock. The - values passed to f will be the new backend database names _after_ - rotation. - */ + /** Atomically replace the current writable backend with @p newBackend. + * + * Performs a three-step pointer swap under the implementation's internal + * mutex: + * 1. Mark the current archive backend for directory deletion and stash it + * in a local `shared_ptr` to keep it alive past the lock release. + * 2. Demote the current writable backend to the archive role. + * 3. Install @p newBackend as the new writable backend. + * + * After releasing the lock, @p f is called with the new backend names. + * Only after @p f returns does the old archive `shared_ptr` go out of + * scope and its on-disk files are removed. This sequencing is + * **crash-safe**: if the process dies between the pointer swap and @p f + * completing, both directory sets still exist on disk and can be + * recovered from the SQL state database on restart. + * + * Concurrent fetches already in flight hold `shared_ptr` references to + * the old backends; reference counting keeps those backends alive until + * all in-flight I/O completes. + * + * @param newBackend Freshly created, opened backend that becomes the new + * writable store; ownership is transferred. + * @param f Callback invoked after the in-memory swap completes but + * before the old archive is deleted, and outside the implementation + * mutex. Receives two names post-rotation: @p writableName is the + * name of @p newBackend, and @p archiveName is the name of the former + * writable backend now serving as the archive. `SHAMapStoreImp` uses + * @p f to durably persist the new backend names and `lastRotated` + * ledger sequence to a SQL state database, creating an atomic + * checkpoint for crash recovery. + */ virtual void rotate( std::unique_ptr&& newBackend, diff --git a/include/xrpl/nodestore/DummyScheduler.h b/include/xrpl/nodestore/DummyScheduler.h index 472684ff13..74748ad439 100644 --- a/include/xrpl/nodestore/DummyScheduler.h +++ b/include/xrpl/nodestore/DummyScheduler.h @@ -4,16 +4,64 @@ namespace xrpl::NodeStore { -/** Simple NodeStore Scheduler that just performs the tasks synchronously. */ +/** Null-object implementation of @ref Scheduler for tests and offline import. + * + * Satisfies the full `Scheduler` interface contract while doing the minimum + * possible work: every task is executed immediately on the calling thread, and + * the two performance-reporting hooks are no-ops. There is no thread pool, no + * queue, and no statistics collection. + * + * The `Scheduler` contract explicitly permits running a task on the calling + * thread, so `DummyScheduler` is always correct — it differs from a + * production scheduler only in latency and throughput characteristics. + * + * **Effect on `BatchWriter`**: Because `scheduleTask` flushes the batch + * inline before returning, batching is effectively disabled. This is + * acceptable for import and test workloads but would degrade performance + * under normal ledger-processing load. + * + * **Typical call sites**: + * - `Application.cpp` — transient scheduler for the source database during + * node-startup `doImport`; sequential offline migration makes async + * scheduling unnecessary. + * - `Backend_test.cpp`, `Database_test.cpp`, `Timing_test.cpp`, + * `NuDBFactory_test.cpp`, `shamap/common.h` — test fixtures use this to + * obtain deterministic, single-threaded execution without the teardown + * complexity of a real async scheduler. + * + * @see Scheduler + * @see BatchWriter + */ class DummyScheduler : public Scheduler { public: DummyScheduler() = default; ~DummyScheduler() override = default; + + /** Execute @p task synchronously on the calling thread. + * + * Calls `task.performScheduledTask()` directly and returns only after + * the task completes. With `BatchWriter` as the consumer, this causes + * the pending write batch to be flushed inline, disabling asynchronous + * batching. + * + * @param task The task to execute; must remain valid for the duration + * of the call. + */ void scheduleTask(Task& task) override; + + /** No-op performance hook — fetch telemetry is not collected. + * + * @param report Ignored. + */ void onFetch(FetchReport const& report) override; + + /** No-op performance hook — batch-write telemetry is not collected. + * + * @param report Ignored. + */ void onBatchWrite(BatchWriteReport const& report) override; }; diff --git a/include/xrpl/nodestore/Factory.h b/include/xrpl/nodestore/Factory.h index c40be62d21..7b661f1479 100644 --- a/include/xrpl/nodestore/Factory.h +++ b/include/xrpl/nodestore/Factory.h @@ -9,24 +9,55 @@ namespace xrpl::NodeStore { -/** Base class for backend factories. */ +/** Abstract factory for constructing pluggable NodeStore `Backend` instances. + * + * Each concrete subclass wraps one storage engine (NuDB, RocksDB, memory, + * null). Subclasses register themselves with the `Manager` singleton at + * program startup by calling `Manager::insert(*this)` from their constructor, + * typically via a module-level `register*Factory()` free function that holds + * the factory as a function-local static. `Manager::find()` then resolves the + * `type=` configuration string to the matching factory by name. + * + * @note Factory objects are stored as raw (non-owning) pointers in + * `ManagerImp`. Concrete factories registered as function-local statics + * have program lifetime and must outlive the `Manager`. + * + * @see Backend, Manager + */ class Factory { public: virtual ~Factory() = default; - /** Retrieve the name of this factory. */ + /** Return the configuration type string that identifies this backend. + * + * The returned name is used as the lookup key by `Manager::find()`, + * which compares case-insensitively against the `type=` value in the + * `[node_db]` config section (e.g., `"NuDB"`, `"RocksDB"`, `"memory"`). + * + * @return The backend type name (e.g., `"NuDB"`). + */ [[nodiscard]] virtual std::string getName() const = 0; - /** Create an instance of this factory's backend. - - @param keyBytes The fixed number of bytes per key. - @param parameters A set of key/value configuration pairs. - @param burstSize Backend burst size in bytes. - @param scheduler The scheduler to use for running tasks. - @return A pointer to the Backend object. - */ + /** Construct a Backend from configuration, without a shared NuDB context. + * + * The returned backend has not yet been opened; the caller must invoke + * `Backend::open()` before performing any I/O. In production, this is + * done by `ManagerImp::makeDatabase()`. + * + * @param keyBytes Fixed width of every storage key in bytes. Always 32 + * (SHA-512 Half) in production; may differ in tests. + * @param parameters Key/value pairs from the `[node_db]` config section, + * supplying backend-specific settings such as `path` and + * `nudb_block_size`. + * @param burstSize Maximum bytes the backend may buffer before flushing. + * Flows directly into NuDB's `db_.set_burst()` after open; other + * backends may use or ignore it. + * @param scheduler Async task dispatcher for background write jobs. + * @param journal Logging sink for backend diagnostics. + * @return An unopened, uniquely-owned Backend instance. + */ virtual std::unique_ptr createInstance( size_t keyBytes, @@ -35,15 +66,24 @@ public: Scheduler& scheduler, beast::Journal journal) = 0; - /** Create an instance of this factory's backend. - - @param keyBytes The fixed number of bytes per key. - @param parameters A set of key/value configuration pairs. - @param burstSize Backend burst size in bytes. - @param scheduler The scheduler to use for running tasks. - @param context The context used by database. - @return A pointer to the Backend object. - */ + /** Construct a Backend sharing an existing NuDB I/O context. + * + * This overload is provided for NuDB backends that share a `nudb::context` + * thread pool across multiple backends (e.g., the rotating database used + * for shard imports). Non-NuDB factories inherit a default implementation + * that returns an empty `unique_ptr`, signaling to `ManagerImp` that this + * backend does not use a NuDB context; the caller falls back to the + * context-free overload in that case. + * + * @param keyBytes Fixed width of every storage key in bytes. + * @param parameters Key/value pairs from the `[node_db]` config section. + * @param burstSize Maximum bytes the backend may buffer before flushing. + * @param scheduler Async task dispatcher for background write jobs. + * @param context Shared NuDB I/O thread pool. Ignored by non-NuDB backends. + * @param journal Logging sink for backend diagnostics. + * @return An unopened Backend, or an empty `unique_ptr` if this factory + * does not support the NuDB context overload. + */ virtual std::unique_ptr createInstance( size_t keyBytes, diff --git a/include/xrpl/nodestore/Manager.h b/include/xrpl/nodestore/Manager.h index 1c4e5b63cf..9e6a784054 100644 --- a/include/xrpl/nodestore/Manager.h +++ b/include/xrpl/nodestore/Manager.h @@ -5,7 +5,28 @@ namespace xrpl::NodeStore { -/** Singleton for managing NodeStore factories and back ends. */ +/** Abstract interface for the NodeStore backend registry and factory. + * + * `Manager` maps the `type=` string from `[node_db]` in `xrpld.cfg` to the + * concrete `Backend` implementation that implements it, and exposes the two + * construction entry points the rest of the application needs: `makeBackend()` + * for a raw storage engine and `makeDatabase()` for a fully-wired `Database`. + * + * The concrete implementation is `ManagerImp`, a Meyers singleton returned by + * `instance()`. Its constructor eagerly registers the four built-in backends + * (NuDB, RocksDB, memory, null). Additional backends may be registered at + * runtime via `insert()`. The abstract base class is exposed here so callers + * depend only on the interface without being coupled to `ManagerImp` or its + * dependencies. + * + * All registry operations (`insert`, `erase`, `find`) are protected by an + * internal mutex and are safe to call concurrently. + * + * @note Copy construction and copy assignment are deleted — there is exactly + * one manager for the lifetime of the process. + * + * @see Factory, Backend, Database + */ class Manager { public: @@ -15,26 +36,81 @@ public: Manager& operator=(Manager const&) = delete; - /** Returns the instance of the manager singleton. */ + /** Return the process-wide Manager singleton. + * + * Delegates to `ManagerImp::instance()`, which uses a Meyers static local + * for thread-safe, once-only initialization under C++11 and later. The + * four built-in backends are registered before the reference is returned + * for the first time. + * + * @return A reference to the singleton `ManagerImp`. + */ static Manager& instance(); - /** Add a factory. */ + /** Register a backend factory with the manager. + * + * After insertion, `find(factory.getName())` will return `&factory`. The + * call is protected by a mutex and safe to make concurrently. The manager + * stores a non-owning pointer; the caller is responsible for ensuring the + * factory outlives the manager (function-local statics are the idiomatic + * approach). + * + * @param factory The factory to register. Must remain alive for the + * lifetime of the manager. + */ virtual void insert(Factory& factory) = 0; - /** Remove a factory. */ + /** Deregister a previously inserted backend factory. + * + * Removes `factory` from the internal list. The call is protected by a + * mutex. Passing a pointer that was never inserted triggers an + * `XRPL_ASSERT`. + * + * @note Built-in backend factories registered by `ManagerImp`'s + * constructor are intentionally never erased: because static-storage + * destruction order across translation units is undefined, calling + * `erase()` from a `Factory` destructor could invoke a destroyed + * `ManagerImp`. The built-in factories use function-local statics + * that outlive the manager. + * + * @param factory The factory to remove. Must have been previously passed + * to `insert()`. + */ virtual void erase(Factory& factory) = 0; - /** Return a pointer to the matching factory if it exists. - @param name The name to match, performed case-insensitive. - @return `nullptr` if a match was not found. - */ + /** Look up a factory by its type name. + * + * Comparison is case-insensitive (via `boost::iequals`), so `"NuDB"`, + * `"nudb"`, and `"NUDB"` all resolve to the same factory. The call is + * protected by a mutex. + * + * @param name The backend type name to search for (e.g., `"NuDB"`). + * @return Pointer to the matching `Factory`, or `nullptr` if none found. + */ virtual Factory* find(std::string const& name) = 0; - /** Create a backend. */ + /** Construct an unopened Backend from configuration parameters. + * + * Reads the `type` key from `parameters`, resolves it to a registered + * `Factory` via `find()`, and delegates to `Factory::createInstance()`. + * The returned backend has not yet been opened; the caller must invoke + * `Backend::open()` before performing any I/O (this is done automatically + * by `makeDatabase()`). + * + * @param parameters Key/value pairs from the `[node_db]` config section. + * Must contain a `type` key naming a registered backend. + * @param burstSize Maximum bytes the backend may buffer before flushing. + * @param scheduler Async task dispatcher for background write jobs. + * @param journal Logging sink for backend diagnostics. + * @return A uniquely-owned, unopened Backend instance. + * @throws std::runtime_error If the `type` key is absent or names an + * unrecognised backend, with a message directing the operator to + * check `xrpld.cfg`. + */ virtual std::unique_ptr makeBackend( Section const& parameters, @@ -42,34 +118,25 @@ public: Scheduler& scheduler, beast::Journal journal) = 0; - /** Construct a NodeStore database. - - The parameters are key value pairs passed to the backend. The - 'type' key must exist, it defines the choice of backend. Most - backends also require a 'path' field. - - Some choices for 'type' are: - HyperLevelDB, LevelDBFactory, SQLite, MDB - - If the fastBackendParameter is omitted or empty, no ephemeral database - is used. If the scheduler parameter is omitted or unspecified, a - synchronous scheduler is used which performs all tasks immediately on - the caller's thread. - - @note If the database cannot be opened or created, an exception is - thrown. - - @param name A diagnostic label for the database. - @param burstSize Backend burst size in bytes. - @param scheduler The scheduler to use for performing asynchronous tasks. - @param readThreads The number of async read threads to create - @param backendParameters The parameter string for the persistent - backend. - @param fastBackendParameters [optional] The parameter string for the - ephemeral backend. - - @return The opened database. - */ + /** Construct and open a fully-wired Database backed by a single backend. + * + * Calls `makeBackend()` to create and open the backend, then wraps it in + * a `DatabaseNodeImp` which adds an async read-thread pool and the full + * `Database` API. The `backendParameters` section must contain a `type` + * key naming a registered backend; most backends also require a `path` + * key. Currently registered built-in types are: `NuDB`, `RocksDB`, + * `memory`, `none`. + * + * @param burstSize Maximum bytes the backend may buffer before flushing. + * @param scheduler Async task dispatcher for read and write jobs. + * @param readThreads Number of threads in the async read pool. + * @param backendParameters Key/value pairs for the persistent backend, + * including at minimum a `type` key. + * @param journal Logging sink for database diagnostics. + * @return A uniquely-owned, open Database ready for I/O. + * @throws std::runtime_error If the backend cannot be created or opened, + * or if the `type` key is missing or unrecognised. + */ virtual std::unique_ptr makeDatabase( std::size_t burstSize, diff --git a/include/xrpl/nodestore/NodeObject.h b/include/xrpl/nodestore/NodeObject.h index 2a216606c4..02359ca2f1 100644 --- a/include/xrpl/nodestore/NodeObject.h +++ b/include/xrpl/nodestore/NodeObject.h @@ -1,72 +1,126 @@ +/** @file + * Defines `NodeObject`, the atomic storage unit of the XRPL node store. + * + * Every piece of ledger state — account tree nodes, transaction tree nodes, + * and ledger headers — is stored and retrieved as a `NodeObject`. The class + * is a pure value type: a type tag, a 256-bit hash key, and a raw binary + * blob. Higher layers (SHAMap, ledger, serialization) are responsible for + * interpreting the blob's contents. + * + * `NodeObject` lives in the `xrpl` namespace rather than `xrpl::NodeStore` + * so that the SHAMap layer, ledger subsystem, and serialization paths can + * consume it without pulling in the full nodestore backend API. + */ #pragma once #include #include #include -// VFALCO NOTE Intentionally not in the NodeStore namespace - namespace xrpl { -/** The types of node objects. */ +/** Identifies the kind of data stored in a `NodeObject`. + * + * The integer values are part of the on-disk format (written by + * `EncodedBlob` and read by `DecodedBlob`), so they must not be changed. + * Value 2 is a historical gap left by a removed type and must remain + * unused. `Dummy` (512) is deliberately outside the contiguous valid range + * so it cannot be confused with a legitimate type by accident or by + * off-by-one arithmetic; it is used as a cache sentinel meaning "confirmed + * missing". + */ enum class NodeObjectType : std::uint32_t { - Unknown = 0, - Ledger = 1, - AccountNode = 3, - TransactionNode = 4, - Dummy = 512 // an invalid or missing object + Unknown = 0, /**< Type not yet determined or not applicable. */ + Ledger = 1, /**< Serialized ledger header. */ + // Value 2 intentionally absent — historical removal; do not reuse. + AccountNode = 3, /**< SHAMap node from an account-state tree. */ + TransactionNode = 4, /**< SHAMap node from a transaction tree. */ + Dummy = 512 /**< Sentinel for a confirmed-missing cache entry; not a real object. */ }; -/** A simple object that the Ledger uses to store entries. - NodeObjects are comprised of a type, a hash, and a blob. - They can be uniquely identified by the hash, which is a half-SHA512 of - the blob. The blob is a variable length block of serialized data. The - type identifies what the blob contains. - - @note No checking is performed to make sure the hash matches the data. - @see SHAMap -*/ +/** Immutable storage unit carrying a type tag, a 256-bit hash key, and a + * raw binary payload. + * + * `NodeObject` is the payload type at every level of the nodestore stack: + * `Backend::fetch()` produces instances; `Backend::store()` and + * `Backend::storeBatch()` consume them; `Database` caches shared pointers + * to them. All three data members are `const` — once constructed the + * object never changes, which is correct for content-addressed storage. + * + * Instances must be created exclusively through `createObject()`. Direct + * construction is blocked via the `PrivateAccess` tag idiom (see below). + * All shared references are `std::shared_ptr`; ownership is + * always shared, never transferred. + * + * Inherits `CountedObject` to maintain a global atomic + * live-instance count that feeds the `get_counts` diagnostic RPC. + * + * @note The hash is accepted on trust — no verification that it matches + * the payload is performed here. Correctness is enforced at higher + * layers (SHAMap traversal, ledger validation). + * @see SHAMap + */ class NodeObject : public CountedObject { public: + /** Size in bytes of the hash key used to identify a `NodeObject`. */ static constexpr std::size_t kKEY_BYTES = 32; private: - // This hack is used to make the constructor effectively private - // except for when we use it in the call to make_shared. - // There's no portable way to make make_shared<> a friend work. + /** Tag type that makes the public constructor effectively private. + * + * `std::make_shared` requires the constructor it calls to be + * accessible, so the constructor cannot be `private`. Instead, it + * takes a `PrivateAccess` argument. Because `PrivateAccess` itself is + * a private nested type, only code inside `NodeObject` (i.e., + * `createObject`) can construct one — achieving the same effect. + */ struct PrivateAccess { explicit PrivateAccess() = default; }; public: - // This constructor is private, use createObject instead. + /** Constructs a `NodeObject`; use `createObject()` instead. + * + * The `PrivateAccess` parameter is intentionally inaccessible to + * external callers; it exists solely to satisfy `std::make_shared`. + */ NodeObject(NodeObjectType type, Blob&& data, uint256 const& hash, PrivateAccess); - /** Create an object from fields. - - The caller's variable is modified during this call. The - underlying storage for the Blob is taken over by the NodeObject. - - @param type The type of object. - @param ledgerIndex The ledger in which this object appears. - @param data A buffer containing the payload. The caller's variable - is overwritten. - @param hash The 256-bit hash of the payload data. - */ + /** Create a `NodeObject`, transferring ownership of the payload buffer. + * + * The caller's `data` buffer is moved into the new object; after this + * call `data` is in a valid but unspecified state. No copy of the + * payload is made. + * + * @param type The kind of ledger data the payload represents. + * @param data Raw serialized payload; ownership is transferred to the + * returned object. + * @param hash 256-bit hash that uniquely identifies this object in the + * node store. Must be the correct hash of `data` — no verification + * is performed. + * @return A `shared_ptr` to the newly created, immutable `NodeObject`. + */ static std::shared_ptr createObject(NodeObjectType type, Blob&& data, uint256 const& hash); - /** Returns the type of this object. */ + /** Returns the type tag indicating what kind of ledger data this object + * holds. + */ [[nodiscard]] NodeObjectType getType() const; - /** Returns the hash of the data. */ + /** Returns the 256-bit hash that identifies this object in the node + * store. + * + * @note The hash is not verified against the payload at construction + * time; callers must ensure consistency at higher layers. + */ [[nodiscard]] uint256 const& getHash() const; - /** Returns the underlying data. */ + /** Returns the raw serialized payload stored in this object. */ [[nodiscard]] Blob const& getData() const; diff --git a/include/xrpl/nodestore/Scheduler.h b/include/xrpl/nodestore/Scheduler.h index 588ff19bdc..a688f1c580 100644 --- a/include/xrpl/nodestore/Scheduler.h +++ b/include/xrpl/nodestore/Scheduler.h @@ -6,59 +6,120 @@ namespace xrpl::NodeStore { -enum class FetchType { Synchronous, Async }; +/** Distinguishes how a node-object fetch was initiated. + * + * Used by `FetchReport` to let the `Scheduler` route telemetry to the + * correct load-tracking bucket (`jtNS_SYNC_READ` vs `jtNS_ASYNC_READ` + * in production). + */ +enum class FetchType { + Synchronous, /**< Fetch was issued on the caller's thread and awaited inline. */ + Async /**< Fetch was queued and completed on a background read thread. */ +}; -/** Contains information about a fetch operation. */ +/** Performance telemetry for a single completed node-object fetch. + * + * Created on the stack immediately before a fetch and passed to + * `Scheduler::onFetch()` once the fetch returns. `fetchType` is fixed at + * construction; `elapsed` and `wasFound` are filled in afterwards. + * + * @see Scheduler::onFetch + */ struct FetchReport { + /** Construct a report for a fetch of the given type. + * + * @param fetchType Whether the fetch was synchronous or asynchronous; + * stored as a `const` member and cannot be changed after construction. + */ explicit FetchReport(FetchType fetchType) : fetchType(fetchType) { } - std::chrono::milliseconds elapsed{}; - FetchType const fetchType; - bool wasFound = false; + std::chrono::milliseconds elapsed{}; /**< Wall-clock duration of the fetch; zero-initialized. */ + FetchType const fetchType; /**< Sync or async; set at construction. */ + bool wasFound = false; /**< True if the object was present in the backend. */ }; -/** Contains information about a batch write operation. */ +/** Performance telemetry for a single completed batch write. + * + * Constructed by `BatchWriter` after each flush and passed to + * `Scheduler::onBatchWrite()`. Both fields must be filled in by the caller + * before the report is forwarded. + * + * @see Scheduler::onBatchWrite + */ struct BatchWriteReport { explicit BatchWriteReport() = default; - std::chrono::milliseconds elapsed; - int writeCount; + std::chrono::milliseconds elapsed; /**< Wall-clock duration of the batch flush. */ + int writeCount; /**< Number of `NodeObject`s written in this batch. */ }; -/** Scheduling for asynchronous backend activity - - For improved performance, a backend has the option of performing writes - in batches. These writes can be scheduled using the provided scheduler - object. - - @see BatchWriter -*/ +/** Scheduling and telemetry interface for NodeStore backend activity. + * + * Decouples backend write batching and I/O instrumentation from any + * particular threading strategy. A `Scheduler` implementation may run a + * submitted task synchronously on the calling thread (as `DummyScheduler` + * does) or post it to a thread pool (as `NodeStoreScheduler` does via the + * application `JobQueue`). The same backend code is correct under either + * policy. + * + * The interface serves two orthogonal purposes that share one injection + * point: *work dispatch* (`scheduleTask`) and *telemetry ingestion* + * (`onFetch`, `onBatchWrite`). Concrete implementations may ignore the + * telemetry hooks entirely or forward them to a load-balancing subsystem. + * + * @note `scheduleTask` takes `task` by non-const reference rather than by + * value or smart pointer. `BatchWriter` implements `Task` privately and + * manages its own lifetime, so no heap allocation is required for the + * common write-batching case. Callers must ensure the task object + * remains valid until `performScheduledTask()` returns. + * + * @see BatchWriter + * @see DummyScheduler + */ class Scheduler { public: virtual ~Scheduler() = default; - /** Schedules a task. - Depending on the implementation, the task may be invoked either on - the current thread of execution, or an unspecified - implementation-defined foreign thread. - */ + /** Dispatch a task for execution. + * + * The scheduler may call `task.performScheduledTask()` on the current + * thread before returning, or post the task to an unspecified foreign + * thread. Both behaviours are valid; callers must not assume which will + * occur. The task object must remain valid until `performScheduledTask()` + * returns. + * + * @param task The deferred work to execute; typically a `BatchWriter` + * flush. Passed by reference — ownership is not transferred. + */ virtual void scheduleTask(Task& task) = 0; - /** Reports completion of a fetch - Allows the scheduler to monitor the node store's performance - */ + /** Telemetry hook called after each node-object fetch completes. + * + * Allows the scheduler to record I/O latency and hit/miss statistics. + * This is a pure reporting path with no effect on control flow; backends + * call it unconditionally after every fetch, whether or not the object + * was found. + * + * @param report Timing, fetch type, and hit/miss outcome for the + * completed fetch. + */ virtual void onFetch(FetchReport const& report) = 0; - /** Reports the completion of a batch write - Allows the scheduler to monitor the node store's performance - */ + /** Telemetry hook called after each batch write completes. + * + * Allows the scheduler to record write throughput. Called by + * `BatchWriter` after each flush, with `report.writeCount` reflecting + * the number of objects flushed in that batch. + * + * @param report Elapsed time and object count for the completed batch. + */ virtual void onBatchWrite(BatchWriteReport const& report) = 0; }; diff --git a/include/xrpl/nodestore/Task.h b/include/xrpl/nodestore/Task.h index 0695970a68..7e7c41fe34 100644 --- a/include/xrpl/nodestore/Task.h +++ b/include/xrpl/nodestore/Task.h @@ -1,15 +1,53 @@ +/** @file + * Defines the `Task` abstract interface for NodeStore scheduled work units. + * + * Any piece of deferred backend work (e.g., a `BatchWriter` flush) inherits + * from `Task` and implements `performScheduledTask()`. The `Scheduler` + * interface accepts a `Task&` and decides *where* and *when* to invoke it, + * decoupling the work unit from any knowledge of threads or job queues. + */ + #pragma once namespace xrpl::NodeStore { -/** Derived classes perform scheduled tasks. */ +/** Pure command-pattern base for NodeStore deferred backend work. + * + * A `Task` is the minimal callable token the scheduling system needs: a single + * `performScheduledTask()` entry point and a virtual destructor. Concrete work + * units inherit from this struct (typically privately, as `BatchWriter` does) + * and are submitted to `Scheduler::scheduleTask()`. + * + * The scheduling contract is intentionally loose: `Scheduler::scheduleTask()` + * may invoke the task synchronously on the calling thread (as `DummyScheduler` + * does for tests) or post it to an unspecified foreign thread (as + * `NodeStoreScheduler` does via the application `JobQueue`). Concrete `Task` + * implementations must be safe under either policy. + * + * The interface is deliberately as small as possible. A richer alternative + * such as `std::function` or `std::unique_ptr` would impose a heap + * allocation on every scheduled operation and couple the interface to a + * specific ownership model. With this design, `BatchWriter` can implement + * `Task` privately and pass `*this` to `scheduleTask()` — no extra allocation + * needed, and lifetime management stays entirely within `BatchWriter`. + * + * @see Scheduler + * @see BatchWriter + * @see DummyScheduler + */ struct Task { virtual ~Task() = default; - /** Performs the task. - The call may take place on a foreign thread. - */ + /** Execute the deferred work represented by this task. + * + * Called by the `Scheduler` either synchronously on the submitting thread + * or asynchronously on a foreign thread, depending on the scheduler + * implementation. Implementations must tolerate either calling context. + * + * The object must remain valid and unmodified from the time it is passed + * to `Scheduler::scheduleTask()` until this method returns. + */ virtual void performScheduledTask() = 0; }; diff --git a/include/xrpl/nodestore/Types.h b/include/xrpl/nodestore/Types.h index cf1a9db42e..1afe8de560 100644 --- a/include/xrpl/nodestore/Types.h +++ b/include/xrpl/nodestore/Types.h @@ -1,3 +1,13 @@ +/** @file + * Shared vocabulary types for the xrpl::NodeStore subsystem. + * + * This header sits at the base of the NodeStore include hierarchy and is + * pulled in by every other NodeStore interface header. It defines only the + * primitives that all participants — backends, the async database layer, and + * callers — must agree on: the operation status codes, the batch container + * alias, and the batch-size policy constants. + */ + #pragma once #include @@ -6,29 +16,57 @@ namespace xrpl::NodeStore { -// This is only used to pre-allocate the array for -// batch objects and does not affect the amount written. -// +/** Initial capacity hint for a `Batch` vector and the backpressure threshold + * in `BatchWriter::store`. + * + * `BatchWriter` reserves this many slots on construction and re-reserves after + * each flush to avoid repeated allocations. `BatchWriter::store` also blocks + * when `writeSet_` reaches this size, providing backpressure against producers + * that outrun the flush thread. This value does not cap how many objects can + * ultimately be written in a single pass. + */ static constexpr auto kBATCH_WRITE_PREALLOCATION_SIZE = 256; -// This sets a limit on the maximum number of writes -// in a batch. Actual usage can be twice this since -// we have a new batch growing as we write the old. -// +/** Maximum number of objects flushed in a single batch write. + * + * Once a batch accumulates this many objects it is handed off to the backend. + * Because a new batch begins accumulating while the previous one is being + * written to disk (double-buffer pattern), peak in-flight memory for pending + * objects can reach approximately twice this limit. + */ static constexpr auto kBATCH_WRITE_LIMIT_SIZE = 65536; -/** Return codes from Backend operations. */ +/** Return codes from `Backend` fetch and store operations. + * + * Values 0–99 are reserved for the standard codes defined here. Backend + * implementations that need additional error distinctions must use values + * starting at `CustomCode` (100) to avoid collisions. + */ enum class Status { - Ok = 0, - NotFound = 1, - DataCorrupt = 2, - Unknown = 3, - BackendError = 4, + Ok = 0, /**< Operation completed successfully. */ + NotFound = 1, /**< Key is not present in the store. */ + DataCorrupt = 2, /**< Stored blob failed integrity validation. */ + Unknown = 3, /**< An unclassified error occurred. */ + BackendError = 4, /**< The underlying storage backend reported an error. */ + /** First value available for backend-defined extended error codes. + * Backend implementations may define their own codes as + * `static_cast(Status::CustomCode) + N` without colliding with the + * standard range (0–99). + */ CustomCode = 100 }; -/** A batch of NodeObjects to write at once. */ +/** A collection of `NodeObject`s to be written together in a single batch. + * + * Using a named alias rather than spelling out the type at every call site + * means that a change to the container type or ownership model propagates + * from this single definition. The `shared_ptr` element type reflects that + * individual `NodeObject` instances may be concurrently referenced by + * in-memory caches and the write pipeline at the same time. + * + * @see Backend::storeBatch + */ using Batch = std::vector>; } // namespace xrpl::NodeStore diff --git a/include/xrpl/nodestore/detail/BatchWriter.h b/include/xrpl/nodestore/detail/BatchWriter.h index b0383838dc..1fa5510f10 100644 --- a/include/xrpl/nodestore/detail/BatchWriter.h +++ b/include/xrpl/nodestore/detail/BatchWriter.h @@ -9,18 +9,46 @@ namespace xrpl::NodeStore { -/** Batch-writing assist logic. - - The batch writes are performed with a scheduled task. Use of the - class it not required. A backend can implement its own write batching, - or skip write batching if doing so yields a performance benefit. - - @see Scheduler -*/ +/** Coalesces individual NodeObject writes into batches for NodeStore backends. + * + * Individual key-value store writes carry per-operation overhead (system + * call, WAL append, compaction pressure). `BatchWriter` amortises that cost + * by accumulating objects in an internal buffer and flushing them as a single + * batch via a `Scheduler`-dispatched task. Use of this class is optional — + * a backend may implement its own batching strategy or skip batching entirely. + * + * The class privately inherits `Task`, turning itself into a schedulable unit + * of work with no additional heap allocation. The actual write is delegated + * to a `Callback` (typically the owning backend), keeping storage-engine + * specifics out of the batching logic. + * + * **Thread safety**: `store()` and `getWriteLoad()` are safe to call + * concurrently from multiple threads. The flush task may run on the calling + * thread (synchronous scheduler) or a background thread (async scheduler); + * the recursive mutex design is safe under both policies. + * + * **Backpressure**: `store()` blocks when the pending buffer reaches + * `kBATCH_WRITE_LIMIT_SIZE` (65,536 objects), preventing unbounded memory + * growth when disk I/O cannot keep pace with producers. Peak in-flight + * memory can reach approximately twice this limit due to the double-buffer + * swap pattern (one batch being written while the next accumulates). + * + * @see Scheduler + * @see Backend + */ class BatchWriter : private Task { public: - /** This callback does the actual writing. */ + /** Pure interface through which `BatchWriter` delivers a completed batch. + * + * The concrete backend (e.g., `RocksDBBackend`) inherits both `Backend` + * and `BatchWriter::Callback`, implementing `writeBatch` to forward the + * batch to the underlying storage engine. This indirection keeps batching + * logic storage-agnostic. + * + * `writeBatch` is invoked outside the internal mutex, so implementations + * may perform blocking I/O without serialising concurrent `store()` calls. + */ struct Callback { virtual ~Callback() = default; @@ -29,49 +57,111 @@ public: Callback& operator=(Callback const&) = delete; + /** Flush a completed batch to the storage engine. + * + * Called by `BatchWriter` once per scheduled flush, with the lock + * already released. The implementation must persist every object in + * `batch` before returning. + * + * @param batch The collection of `NodeObject`s to write. Objects in + * the batch may be concurrently referenced by in-memory caches. + */ virtual void writeBatch(Batch const& batch) = 0; }; - /** Create a batch writer. */ + /** Construct a `BatchWriter` tied to the given sink and scheduler. + * + * Pre-allocates the internal write buffer to avoid repeated small + * reallocations during normal operation. + * + * @param callback The sink that receives each flushed `Batch` via + * `Callback::writeBatch()`. Typically the owning backend. Must + * outlive this `BatchWriter`. + * @param scheduler The scheduler used to dispatch the flush task. May be + * a synchronous `DummyScheduler` (tests and bulk import) or the + * production async scheduler; both are supported. + */ BatchWriter(Callback& callback, Scheduler& scheduler); - /** Destroy a batch writer. - - Anything pending in the batch is written out before this returns. - */ + /** Destroy the `BatchWriter`, draining any pending writes first. + * + * Blocks until all accumulated objects have been flushed to the + * `Callback`. No objects passed to `store()` are silently abandoned. + */ ~BatchWriter() override; - /** Store the object. - - This will add to the batch and initiate a scheduled task to - write the batch out. - */ + /** Enqueue a `NodeObject` for the next scheduled batch flush. + * + * Appends `object` to the internal accumulation buffer and, if no flush + * task is already outstanding, schedules one via the `Scheduler`. + * Subsequent `store()` calls before the flush fires piggyback on the + * single in-flight task. + * + * @param object The `NodeObject` to persist. + * @note Blocks the caller when the buffer reaches `kBATCH_WRITE_LIMIT_SIZE` + * (65,536 objects) until the in-flight batch is fully written. This + * backpressure prevents unbounded memory growth when disk I/O falls + * behind producers. + */ void store(std::shared_ptr const& object); - /** Get an estimate of the amount of writing I/O pending. */ + /** Return a conservative estimate of pending write I/O. + * + * Returns the larger of the item count currently being written to the + * backend and the item count waiting for the next scheduled flush. + * Taking the maximum reflects pressure in both the in-flight and + * accumulating phases, giving callers a meaningful load signal for + * scheduling decisions. + * + * @return Estimated number of `NodeObject`s awaiting or undergoing write. + */ int getWriteLoad(); private: + /** `Task` entry-point; delegates to the internal `writeBatch()`. */ void performScheduledTask() override; + + /** Drain accumulated objects to the backend using the double-buffer swap. + * + * Holds the lock only long enough to swap the internal buffer with a + * local vector (O(1)), then releases the lock before calling + * `Callback::writeBatch()`. Loops until no objects remain after a swap, + * then clears `writePending_` and notifies any blocked `store()` callers. + */ void writeBatch(); + + /** Block until any in-flight flush has completed. + * + * Waits on the condition variable until `writePending_` is false. + * Called by the destructor to guarantee no pending objects are abandoned + * on teardown. + */ void waitForWriting(); private: + /** Recursive to allow synchronous schedulers that invoke `writeBatch()` + * on the same thread as `store()` or `waitForWriting()`. */ using LockType = std::recursive_mutex; + + /** Required by `LockType`; `std::condition_variable` only works with + * `std::mutex`. */ using CondvarType = std::condition_variable_any; Callback& callback_; Scheduler& scheduler_; LockType writeMutex_; CondvarType writeCondition_; + /** Item count of the batch currently being written; used by `getWriteLoad()`. */ int writeLoad_{0}; + /** True when a flush task has been scheduled but not yet completed. */ bool writePending_{false}; + /** Accumulation buffer; swapped out atomically inside `writeBatch()`. */ Batch writeSet_; }; diff --git a/include/xrpl/nodestore/detail/DatabaseNodeImp.h b/include/xrpl/nodestore/detail/DatabaseNodeImp.h index add66c51a7..920c928b15 100644 --- a/include/xrpl/nodestore/detail/DatabaseNodeImp.h +++ b/include/xrpl/nodestore/detail/DatabaseNodeImp.h @@ -1,3 +1,13 @@ +/** @file + * Single-backend concrete implementation of the NodeStore `Database` interface. + * + * `DatabaseNodeImp` is the standard node-store path for deployments that keep + * all ledger objects in one persistent key/value backend (NuDB, RocksDB, etc.). + * It adapts the thin `Backend` interface onto the richer `Database` contract + * — async read pool, telemetry, and scheduler callbacks — all of which live in + * the base class and cannot be bypassed. + */ + #pragma once #include @@ -6,6 +16,25 @@ namespace xrpl::NodeStore { +/** Single-backend implementation of the NodeStore `Database` interface. + * + * Wraps exactly one `Backend` (NuDB, RocksDB, Memory, Null) and serves all + * ledger objects regardless of their ledger sequence number. This is the + * standard deployment path; the two-backend rotation variant is + * `DatabaseRotatingImp`. + * + * Every public method is a thin delegation: to `backend_` for storage + * operations and to base-class helpers for async dispatch, telemetry, and + * bulk import. No business logic lives here. + * + * **Shutdown ordering**: The destructor calls `stop()` to drain all pending + * async reads and wait for worker threads to exit before releasing `backend_`. + * Worker threads invoke the virtual `fetchNodeObject()` hook; if `backend_` + * were released while a thread was active, it would dereference a dangling + * pointer. + * + * @see Database, DatabaseRotatingImp, Backend + */ class DatabaseNodeImp : public Database { public: @@ -14,6 +43,21 @@ public: DatabaseNodeImp& operator=(DatabaseNodeImp const&) = delete; + /** Construct the database and start the async read thread pool. + * + * Asserts that @p backend is non-null, then delegates to the `Database` + * base constructor which spawns `readThreads` detached worker threads. + * + * @param scheduler Task scheduler for async I/O dispatch and telemetry; + * must outlive this object. + * @param readThreads Number of async prefetch threads; clamped to at least 1 + * by the base constructor. + * @param backend Open, non-null backend to use for all storage; shared + * ownership is assumed. + * @param config `[node_db]` config section; forwarded to `Database` + * for `earliest_seq` and `rq_bundle` parsing. + * @param j Logging sink. + */ DatabaseNodeImp( Scheduler& scheduler, int readThreads, @@ -28,48 +72,126 @@ public: "backend"); } + /** Drain pending I/O and release the backend. + * + * Calls `stop()` to wait for all async read worker threads to exit before + * `backend_` is destroyed. This must happen in the derived destructor + * because worker threads call the virtual `fetchNodeObject()` hook, which + * dereferences `backend_`. + */ ~DatabaseNodeImp() override { stop(); } + /** Return the name of the underlying backend for diagnostics. + * + * @return The backend's human-readable identifier (e.g. the on-disk path). + */ std::string getName() const override { return backend_->getName(); } + /** Return the estimated number of pending write operations in the backend. + * + * Approximate; the value may change immediately after it is read. + * + * @return Pending write count, or 0 if the backend does not batch writes. + */ std::int32_t getWriteLoad() const override { return backend_->getWriteLoad(); } + /** Bulk-import all objects from @p source into this database's backend. + * + * Delegates to `importInternal()`, which iterates @p source via `forEach()` + * and stores objects in batches. Large source databases may take significant + * time; no concurrent writes to @p source should occur during the call. + * + * @param source The database to read from; must remain open and quiescent. + */ void importDatabase(Database& source) override { importInternal(*backend_.get(), source); } + /** Persist a node object to the backend. + * + * Updates store telemetry, wraps the payload in a `NodeObject`, and + * forwards to the backend. The ledger sequence parameter is part of the + * `Database` contract but is ignored here — a single backend holds objects + * from all ledger sequences. + * + * @param type Type tag for the object (ledger, account node, etc.). + * @param data Serialized payload; ownership is transferred — the caller's + * variable is left in a valid but unspecified state. + * @param hash 256-bit content-address key. The caller is responsible for + * correctness; the hash is not re-verified. + */ void store(NodeObjectType type, Blob&& data, uint256 const& hash, std::uint32_t) override; + /** Report whether two ledger sequence numbers map to the same backend. + * + * Always returns `true` for `DatabaseNodeImp` because there is exactly one + * backend: every sequence number resolves to the same physical store. This + * allows the async read pool to coalesce duplicate hash requests that carry + * different sequence numbers without issuing a second backend read. + * + * @return `true` unconditionally. + */ bool isSameDB(std::uint32_t, std::uint32_t) override { - // only one database return true; } + /** Flush any buffered writes to durable storage. + * + * Delegates directly to `backend_->sync()`. Not latency-sensitive; + * typically called on ledger close or maintenance paths. + */ void sync() override { backend_->sync(); } + /** Synchronously fetch a batch of node objects by hash. + * + * Calls `backend_->fetchBatch()` directly, bypassing the async read queue. + * Enforces a positional contract: the returned vector is always the same + * length as @p hashes, with null entries for objects not found. Missing + * objects are logged at `error` level. Wall-clock elapsed time is reported + * via `updateFetchMetrics()`; per-slot hit counts are not tracked here and + * remain the caller's responsibility. + * + * @note The batch-level `Status` from the backend is discarded; object + * availability is inferred entirely from null vs. non-null slots. + * @param hashes Ordered list of 256-bit keys to retrieve. + * @return Vector of the same length as @p hashes; null entries indicate + * objects absent from the backend. + */ std::vector> fetchBatch(std::vector const& hashes); + /** Schedule a non-blocking background fetch for a single node object. + * + * Forwards unconditionally to `Database::asyncFetch()`, which coalesces + * duplicate hash requests and dispatches callbacks from the worker thread + * pool. No per-backend routing is needed for the single-backend case. + * + * @param hash 256-bit key of the object to retrieve. + * @param ledgerSeq Ledger sequence the object belongs to; forwarded for + * hash-coalescing decisions via `isSameDB()`. + * @param callback Invoked on a worker thread with the fetched `NodeObject`, + * or `nullptr` on miss or error. + */ void asyncFetch( uint256 const& hash, @@ -77,13 +199,37 @@ public: std::function const&)>&& callback) override; private: - // Persistent key/value storage + /** The single persistent key/value backend that holds all ledger objects. */ std::shared_ptr backend_; + /** Template Method hook called by the base-class public `fetchNodeObject()`. + * + * Delegates to `backend_->fetch()` with structured error logging: + * `Status::Ok` and `Status::NotFound` are silent; `Status::DataCorrupt` + * logs at `fatal`; any other code logs at `warn`. Exceptions from the + * backend are logged at `fatal` then re-raised via `Rethrow()`. Sets + * `fetchReport.wasFound = true` on a hit to feed the base-class metric. + * The ledger sequence parameter is accepted by the signature but unused. + * + * @param hash 256-bit key to look up. + * @param fetchReport Mutable report; `wasFound` is set on a hit. + * @param duplicate Whether this fetch was deduplicated from another + * in-flight request for the same hash; unused in this implementation. + * @return The fetched `NodeObject`, or `nullptr` on miss or error. + * @throws Any exception propagated from `backend_->fetch()` after logging. + */ std::shared_ptr fetchNodeObject(uint256 const& hash, std::uint32_t, FetchReport& fetchReport, bool duplicate) override; + /** Iterate every object in the backend and invoke @p f for each one. + * + * Used exclusively by `importInternal()` for bulk export. Delegates + * directly to `backend_->forEach()`. Not safe for concurrent access with + * reads or writes; see `Backend::forEach()` for details. + * + * @param f Callback invoked with each `NodeObject`; must not be null. + */ void forEach(std::function)> f) override { diff --git a/include/xrpl/nodestore/detail/DatabaseRotatingImp.h b/include/xrpl/nodestore/detail/DatabaseRotatingImp.h index 39441ef4d8..dedd3d144c 100644 --- a/include/xrpl/nodestore/detail/DatabaseRotatingImp.h +++ b/include/xrpl/nodestore/detail/DatabaseRotatingImp.h @@ -6,6 +6,24 @@ namespace xrpl::NodeStore { +/** Concrete two-backend node store that enables online deletion of old ledger data. + * + * Maintains a _writable_ backend (receives all new stores) and an _archive_ + * backend (holds older data). The `SHAMapStore` sweep thread drives rotations: + * when the configured deletion horizon is reached it calls `rotate()`, which + * atomically promotes the current writable to archive, installs a fresh backend + * as the new writable, and schedules the old archive for deletion. + * + * All public methods follow a capture-under-lock / use-outside-lock pattern: + * the mutex protects only the `shared_ptr` swap, not the backend I/O. This + * keeps unrelated readers and writers concurrent during disk operations. + * + * **Thread safety**: all public methods are safe to call from any thread + * concurrently. `stop()` must be called in the derived destructor before + * the base `Database` destructor tears down the async read pool. + * + * @see DatabaseRotating, Database, SHAMapStoreImp + */ class DatabaseRotatingImp : public DatabaseRotating { public: @@ -14,6 +32,20 @@ public: DatabaseRotatingImp& operator=(DatabaseRotatingImp const&) = delete; + /** Construct the rotating database and initialise the async read pool. + * + * Both backends must already be open. Their `fdRequired()` values are + * accumulated into `fdRequired_` so the application can pre-validate the + * process file-descriptor limit before any I/O begins. + * + * @param scheduler Task scheduler for async dispatch and telemetry; + * must outlive this object. + * @param readThreads Number of async read worker threads to spawn. + * @param writableBackend The backend that receives all new stores. + * @param archiveBackend The backend holding older (pre-rotation) data. + * @param config `[node_db]` config section forwarded to `Database`. + * @param j Logging sink. + */ DatabaseRotatingImp( Scheduler& scheduler, int readThreads, @@ -22,48 +54,166 @@ public: Section const& config, beast::Journal j); + /** Destroy the rotating database. + * + * Calls `stop()` before the base destructor so that async worker threads + * stop invoking the virtual `fetchNodeObject()` while derived data members + * are still valid. + */ ~DatabaseRotatingImp() override { stop(); } + /** Atomically swap in a new writable backend, demoting the current one. + * + * The rotation sequence under the mutex is: + * 1. Mark the existing archive backend for on-disk deletion, move it into + * a local to extend its lifetime past the callback. + * 2. Promote the current writable backend to become the new archive. + * 3. Install @p newBackend as the writable backend. + * + * The lock is released before @p f is called. This ordering is critical: + * the callback (in production, `SHAMapStoreImp`) persists the new backend + * names to a SQLite state database. The old archive `shared_ptr` remains + * alive on the stack until after @p f returns, so the archive directory is + * deleted only after the persistent state has been updated — making the + * rotation crash-safe. + * + * @param newBackend Freshly prepared backend to install as the new writable. + * Ownership is transferred; the caller's pointer is null on return. + * @param f Callback invoked after the swap, outside the mutex. + * Receives the new writable name and the new archive name (the former + * writable). Must persist these names to durable storage before + * returning so the node can recover the correct layout after a crash. + * @note The callback is invoked outside the mutex, so other methods + * (including `getName()` and even `rotate()`) may be called from within + * @p f without deadlocking. Re-entering `rotate()` from @p f is + * technically safe but should never occur in production code. + */ void rotate( std::unique_ptr&& newBackend, std::function const& f) override; + /** Return the name of the current writable backend. + * + * Acquires the mutex to take a consistent snapshot of `writableBackend_`. + * + * @return A human-readable identifier for the writable backend. + */ std::string getName() const override; + /** Return the estimated pending write count from the writable backend. + * + * Acquires the mutex to snapshot `writableBackend_`, then queries it + * outside the lock. + * + * @return Pending write count; 0 if the backend does not batch writes. + */ std::int32_t getWriteLoad() const override; + /** Bulk-import all objects from @p source into the current writable backend. + * + * Snapshots `writableBackend_` under the mutex, then delegates to + * `importInternal()`. A rotation that occurs concurrently will not affect + * the import — it continues writing to the backend that was writable when + * it started. + * + * @param source Source database to read from; must remain valid and + * quiescent (no concurrent writes) for the duration of the call. + */ void importDatabase(Database& source) override; + /** Return `true`, since both backends form a single logical namespace. + * + * The async read pool calls this to decide whether two in-flight fetches + * for the same hash (with different ledger sequence numbers) can share a + * single backend read. Because the rotating store presents one logical + * keyspace across both tiers, this always returns `true`. + * + * @return Always `true`. + */ bool isSameDB(std::uint32_t, std::uint32_t) override { - // rotating store acts as one logical database return true; } + /** Store a node object in the current writable backend. + * + * Snapshots `writableBackend_` under the mutex, constructs a `NodeObject` + * from the supplied data, then writes it outside the lock. The ledger + * sequence parameter is accepted for interface compatibility but ignored — + * all writes always go to the current writable backend regardless of age. + * + * @param type Semantic type of the object. + * @param data Serialized payload; moved into the backend. + * @param hash 256-bit content hash; not re-verified. + * @param ledgerSeq Ignored; present for `Database` interface compatibility. + */ void store(NodeObjectType type, Blob&& data, uint256 const& hash, std::uint32_t) override; + /** Flush the writable backend to durable storage. + * + * Holds the mutex for the entire sync call. Acceptable because this is a + * maintenance path, not a latency-sensitive read/write path. + */ void sync() override; private: + /** Active backend; receives all new `store()` calls. */ std::shared_ptr writableBackend_; + + /** Read-only backend holding data from before the last rotation. */ std::shared_ptr archiveBackend_; + + /** Guards swaps of `writableBackend_` and `archiveBackend_`. + * Held only for pointer capture or swap — never across I/O. + */ mutable std::mutex mutex_; + /** Two-tier fetch with optional archive-to-writable promotion. + * + * Snapshots both backend pointers under the mutex, then tries the writable + * backend first. On a miss, tries the archive backend. If the object is + * found in the archive and @p duplicate is `true`, the writable pointer is + * refreshed under the mutex (to handle a concurrent rotation) and the + * object is written back into the current writable tier. + * + * Backend errors are handled conservatively: `DataCorrupt` is logged at + * fatal severity and returns `nullptr` (cache miss); unknown status codes + * are logged at warning level; exceptions are logged and rethrown via + * `Rethrow()`. + * + * @param hash 256-bit content hash of the desired object. + * @param ledgerSeq Ignored; accepted for `Database` virtual interface. + * @param fetchReport Out-param; `wasFound` is set to `true` on a hit. + * @param duplicate When `true`, a hit in the archive is promoted to + * the writable backend. + * @return The found `NodeObject`, or `nullptr` on miss or error. + */ std::shared_ptr fetchNodeObject(uint256 const& hash, std::uint32_t, FetchReport& fetchReport, bool duplicate) override; + /** Visit every object in both backends sequentially. + * + * Snapshots both backend pointers under the mutex, then calls + * `writable->forEach(f)` followed by `archive->forEach(f)` outside the + * lock. Used by `importInternal()` during bulk import. + * + * @param f Callable invoked with each `NodeObject`; must not call any + * method that acquires `mutex_` to avoid deadlock. + * @note Not safe to call concurrently with `rotate()` or other writes if + * the backend's `for_each` re-opens the database (e.g. NuDB). + */ void forEach(std::function)> f) override; }; diff --git a/include/xrpl/nodestore/detail/DecodedBlob.h b/include/xrpl/nodestore/detail/DecodedBlob.h index 90a7b6c9cb..5990253b47 100644 --- a/include/xrpl/nodestore/detail/DecodedBlob.h +++ b/include/xrpl/nodestore/detail/DecodedBlob.h @@ -4,30 +4,84 @@ namespace xrpl::NodeStore { -/** Parsed key/value blob into NodeObject components. - - This will extract the information required to construct a NodeObject. It - also does consistency checking and returns the result, so it is possible - to determine if the data is corrupted without throwing an exception. Not - all forms of corruption are detected so further analysis will be needed - to eliminate false negatives. - - @note This defines the database format of a NodeObject! -*/ +/** Deserializes a raw backend key/value buffer into the components of a + * `NodeObject`. + * + * This is the read-direction half of the NodeStore on-disk format, paired + * with `EncodedBlob`. Together they define the canonical binary schema for + * persisted node objects; any format change must be reflected in both classes. + * + * On-disk layout (canonical reference): + * - Bytes 0–7: Unused prefix. Historically stored a ledger index; written + * as eight zero bytes today and silently ignored on read. + * - Byte 8: `NodeObjectType` discriminant (one-byte enum value). + * - Bytes 9+: Raw serialized object payload. + * + * Validation is intentionally minimal and non-throwing: the constructor sets + * an internal success flag rather than raising an exception, allowing callers + * to handle corruption gracefully (see `wasOk()`). Not all corruption is + * detected — this is a fast sanity check, not a cryptographic integrity proof. + * + * `DecodedBlob` holds non-owning pointers into the caller-supplied buffers; + * the backing storage must remain valid until `createObject()` is called or + * the `DecodedBlob` is destroyed. + * + * @note This class defines the database format of a `NodeObject`. + * @see EncodedBlob for the write-direction counterpart. + */ class DecodedBlob { public: - /** Construct the decoded blob from raw data. */ + /** Parse a raw backend buffer into its constituent NodeObject fields. + * + * Validates the on-disk layout without performing any heap allocation. + * `key_` and `objectData_` are set to non-owning pointers into the + * caller-supplied buffers; the actual payload copy is deferred to + * `createObject()`. The caller must keep both buffers alive for the + * lifetime of this object. + * + * Parsing succeeds (`wasOk()` returns `true`) only when `valueBytes > 9` + * and the type byte at offset 8 is one of the four recognised values: + * `hotUNKNOWN`, `hotLEDGER`, `hotACCOUNT_NODE`, or `hotTRANSACTION_NODE`. + * `hotDUMMY` (value 512) and any unrecognised byte leave the object in a + * failed state without throwing. + * + * @param key Pointer to the 32-byte hash that was used as the + * storage key; not validated or dereferenced here. + * @param value Pointer to the raw value buffer retrieved from the + * backend. + * @param valueBytes Total byte length of `value`. Values of 9 or fewer + * bytes produce a failed parse. + */ DecodedBlob(void const* key, void const* value, int valueBytes); - /** Determine if the decoding was successful. */ + /** Returns `true` if the constructor successfully parsed a well-formed + * buffer with a recognised `NodeObjectType`. + * + * Must be checked before calling `createObject()`. Calling `createObject()` + * on a failed `DecodedBlob` fires `XRPL_ASSERT` in debug builds. + */ [[nodiscard]] bool wasOk() const noexcept { return success_; } - /** Create a NodeObject from this data. */ + /** Allocate and return a `NodeObject` from the previously parsed fields. + * + * Copies the payload slice into an owning `Blob` and reconstructs the + * full hash key from the stored pointer. This is the only heap allocation + * in the decode path. The returned `NodeObject` owns its data + * independently, so the caller may release the backend fetch buffer + * immediately after this call returns. + * + * @pre `wasOk()` must return `true`. Calling this on a failed parse fires + * `XRPL_ASSERT` in debug builds; in release builds a null + * `shared_ptr` is returned as a defensive fallback. + * @return A fully constructed `NodeObject`, or `nullptr` if the parse had + * failed (release-build defensive path — callers must always check + * `wasOk()` first). + */ std::shared_ptr createObject(); diff --git a/include/xrpl/nodestore/detail/EncodedBlob.h b/include/xrpl/nodestore/detail/EncodedBlob.h index 343e1720a0..1f74631b47 100644 --- a/include/xrpl/nodestore/detail/EncodedBlob.h +++ b/include/xrpl/nodestore/detail/EncodedBlob.h @@ -11,51 +11,80 @@ namespace xrpl::NodeStore { -/** Convert a NodeObject from in-memory to database format. - - The (suboptimal) database format consists of: - - - 8 prefix bytes which will typically be 0, but don't assume that's the - case; earlier versions of the code would use these bytes to store the - ledger index either once or twice. - - A single byte denoting the type of the object. - - The payload. - - @note This class is typically instantiated on the stack, so the size of - the object does not matter as much as it normally would since the - allocation is, effectively, free. - - We leverage that fact to preallocate enough memory to handle most - payloads as part of this object, eliminating the need for dynamic - allocation. As of this writing ~94% of objects require fewer than - 1024 payload bytes. +/** Serializes a `NodeObject` to the binary wire format expected by storage + * backends (NuDB, RocksDB). + * + * This is the write-direction half of the NodeStore on-disk format, paired + * with `DecodedBlob`. Together they define the canonical binary schema for + * persisted node objects; any format change must be reflected in both classes. + * + * On-disk layout (canonical reference): + * - Bytes 0–7: Eight zero bytes. Historically stored the ledger index; + * zeroed since that field was removed. Readers must not assume all + * zeros — older databases may contain non-zero values here. + * - Byte 8: `NodeObjectType` cast to a single `uint8_t`. + * - Bytes 9+: Raw serialized payload from `NodeObject::getData()`. + * + * The 32-byte `uint256` hash is the storage key and is kept separate from + * the value payload. `getKey()` and `getData()` expose these two pieces as + * `void const*` pointers suitable for direct hand-off to NuDB or RocksDB + * slice APIs. + * + * Instances are intended to be constructed immediately before a backend + * insert call and destroyed immediately after, keeping any heap-allocated + * overflow buffer alive for exactly as long as needed. + * + * @note This class is non-copyable. `ptr_` is a `const` raw pointer whose + * ownership is conditional: it points into the inline `payload_` buffer + * when the serialized size fits within 1033 bytes (~94% of real objects), + * and into a heap buffer otherwise. Copying would require duplicating + * that conditional ownership, so no copy or move constructor is provided. + * + * @see DecodedBlob for the read-direction counterpart. */ class EncodedBlob { - /** The 32-byte key of the serialized object. */ + /** Storage key: the object's 32-byte `uint256` hash. */ std::array key_{}; - /** A pre-allocated buffer for the serialized object. - - The buffer is large enough for the 9 byte prefix and at least - 1024 more bytes. The precise size is calculated automatically - at compile time so as to avoid wasting space on padding bytes. + /** Inline stack buffer covering the 9-byte header plus up to 1024 bytes + * of payload. + * + * Sized at compile time via `boost::alignment::align_up` to the next + * `uint32_t`-aligned boundary, eliminating any trailing padding that a + * naive `9 + 1024` array would incur. When `size_` does not exceed this + * array's capacity, `ptr_` aliases `payload_.data()` and no heap + * allocation occurs. */ std::array payload_{}; - /** The size of the serialized data. */ + /** Total byte length of the serialized value (header + payload). */ std::uint32_t size_; - /** A pointer to the serialized data. - - This may point to the pre-allocated buffer (if it is sufficiently - large) or to a dynamically allocated buffer. + /** Pointer to the serialized value buffer. + * + * Set once at construction and never changed (`const`). Points into + * `payload_` when `size_ <= payload_.size()`, or into a heap allocation + * otherwise. The destructor uses `ptr_ != payload_.data()` to decide + * whether to `delete[]`. */ std::uint8_t* const ptr_; public: + /** Serialize `obj` into the on-disk wire format. + * + * Fills `key_` with the object's hash, writes the 9-byte header into + * `ptr_`, then copies the payload. If the total serialized size exceeds + * the inline `payload_` buffer capacity the constructor heap-allocates + * an exact-fit buffer; otherwise the inline buffer is used directly. + * + * @param obj The node object to serialize. Must be non-null: a null + * `shared_ptr` fires `XRPL_ASSERT` in debug builds and throws + * `std::runtime_error` in all builds. + * @throws std::runtime_error if `obj` is null. + */ explicit EncodedBlob(std::shared_ptr const& obj) : size_([&obj]() { XRPL_ASSERT(obj, "xrpl::NodeStore::EncodedBlob::EncodedBlob : non-null input"); @@ -73,6 +102,14 @@ public: std::copy_n(obj->getHash().data(), obj->getHash().size(), key_.data()); } + /** Releases any heap-allocated overflow buffer. + * + * If `ptr_` points outside `payload_` (i.e., a heap buffer was + * allocated because the serialized size exceeded 1033 bytes), the buffer + * is freed with `delete[]`. An `XRPL_ASSERT` verifies that the pointer + * and size fields are mutually consistent before the free, catching any + * state drift that would otherwise cause a double-free or memory leak. + */ ~EncodedBlob() { XRPL_ASSERT( @@ -85,18 +122,41 @@ public: delete[] ptr_; } + /** Returns a pointer to the 32-byte storage key (the object's hash). + * + * The pointer is valid for the lifetime of this `EncodedBlob` and may + * be passed directly to NuDB or RocksDB key-slice APIs. + * + * @return `void const*` pointing to the 32-byte key buffer. + */ [[nodiscard]] void const* getKey() const noexcept { return static_cast(key_.data()); } + /** Returns the total byte length of the serialized value buffer. + * + * This is `obj->getData().size() + 9`: nine header bytes (eight + * zero-prefix bytes plus the type byte) followed by the raw payload. + * + * @return Byte count of the buffer returned by `getData()`. + */ [[nodiscard]] std::size_t getSize() const noexcept { return size_; } + /** Returns a pointer to the serialized value buffer. + * + * The buffer layout is: eight zero bytes, one `NodeObjectType` byte, + * then the raw object payload. The pointer is valid for the lifetime of + * this `EncodedBlob` and may be passed directly to NuDB compression + * helpers or RocksDB value-slice APIs. + * + * @return `void const*` pointing to `getSize()` bytes of serialized data. + */ [[nodiscard]] void const* getData() const noexcept { diff --git a/include/xrpl/nodestore/detail/ManagerImp.h b/include/xrpl/nodestore/detail/ManagerImp.h index 98aec6459b..cb754a1f6b 100644 --- a/include/xrpl/nodestore/detail/ManagerImp.h +++ b/include/xrpl/nodestore/detail/ManagerImp.h @@ -1,9 +1,37 @@ +/** @file + * Declares `ManagerImp`, the concrete Meyers-singleton implementation of the + * NodeStore `Manager` interface, hidden in `detail/` as an implementation + * detail not intended for direct use outside the nodestore subsystem. + */ #pragma once #include namespace xrpl::NodeStore { +/** Concrete singleton implementation of the NodeStore backend registry. + * + * `ManagerImp` maintains a runtime registry of `Factory` objects and + * orchestrates `Backend` and `Database` construction from configuration data. + * The four built-in backends (NuDB, RocksDB, Memory, Null) are registered + * during construction by calling their respective `register*Factory` free + * functions, each of which holds a function-local static `Factory` that + * self-registers via `insert()`. This avoids relying on global-variable + * destruction order across translation units, which would be undefined + * behaviour if a `Factory` destructor called `erase()` after `ManagerImp` + * had already been destroyed. + * + * The registry is a `std::vector` of non-owning pointers protected + * by a `std::mutex`. Ownership of each `Factory` remains with the static + * storage managed by the `register*Factory` functions, which are guaranteed + * to outlive this singleton. + * + * @note Callers outside the nodestore subsystem should use `Manager::instance()` + * rather than `ManagerImp::instance()` to avoid depending on this + * implementation-detail type. + * + * @see Manager, Factory, DatabaseNodeImp + */ class ManagerImp : public Manager { private: @@ -11,25 +39,55 @@ private: std::vector list_; public: + /** Return the process-wide ManagerImp singleton. + * + * Uses a Meyers function-local static for thread-safe, once-only + * construction under C++11 and later. All four built-in backend factories + * are registered before the reference is returned for the first time. + * + * @return Reference to the single ManagerImp instance. + */ static ManagerImp& instance(); + /** Throw a user-facing error when the backend configuration is absent or + * names an unrecognised type. + * + * Both the missing-`type`-key and the unrecognised-type code paths in + * `makeBackend()` converge on this helper so the operator-facing message + * is consistent. + * + * @throws std::runtime_error Always — message directs the operator to add + * or correct the `[node_db]` section in `xrpld.cfg`. + */ static void missingBackend(); + /** Register all built-in backend factories. + * + * Calls `registerNuDBFactory`, `registerRocksDBFactory`, + * `registerNullFactory`, and `registerMemoryFactory`. Each function + * creates a function-local static `Factory` that calls `insert()` on this + * manager. The function-local-static lifetime guarantee ensures all + * factories are destroyed before this `ManagerImp`. + */ ManagerImp(); ~ManagerImp() override = default; + /** @copydoc Manager::find */ Factory* find(std::string const& name) override; + /** @copydoc Manager::insert */ void insert(Factory& factory) override; + /** @copydoc Manager::erase */ void erase(Factory& factory) override; + /** @copydoc Manager::makeBackend */ std::unique_ptr makeBackend( Section const& parameters, @@ -37,6 +95,7 @@ public: Scheduler& scheduler, beast::Journal journal) override; + /** @copydoc Manager::makeDatabase */ std::unique_ptr makeDatabase( std::size_t burstSize, diff --git a/include/xrpl/nodestore/detail/codec.h b/include/xrpl/nodestore/detail/codec.h index 4dedc54902..4681b6e95e 100644 --- a/include/xrpl/nodestore/detail/codec.h +++ b/include/xrpl/nodestore/detail/codec.h @@ -1,3 +1,33 @@ +/** @file + * Compression codec for NodeStore blobs written to and read from NuDB. + * + * Every `NodeObject` value stored in the NuDB backend passes through either + * `nodeobjectCompress` or `nodeobjectDecompress`. The on-disk format is a + * leading varint type tag followed by a type-specific payload: + * + * | Tag | Format | + * |-----|--------| + * | 0 | Uncompressed (legacy; readable but never written) | + * | 1 | LZ4-compressed payload | + * | 2 | Sparse inner-node (16-bit presence bitmask + non-zero hashes) | + * | 3 | Full inner-node (all 16 hashes, no bitmask) | + * + * SHAMap inner nodes (exactly 525 bytes with `HashPrefix::InnerNode`) receive + * a specialized encoding that out-performs LZ4 on their typical hash density. + * All other objects are LZ4-compressed (type 1). The codec reconstructs inner + * nodes with `index`, `unused`, and `kind` fields zeroed, so those fields are + * not preserved across a round-trip. + * + * All functions follow the `BufferFactory` pattern: callers supply a callable + * `void*(std::size_t)` that allocates output memory. The codec never frees + * memory; ownership remains with the caller's factory object. + * + * @note This header is an implementation detail of the NuDB backend and the + * NodeStore import tool. It is not part of the public NodeStore API. + * + * @see nodeobjectCompress, nodeobjectDecompress, filterInner + */ + #pragma once // Disable lz4 deprecation warning due to incompatibility with clang attributes @@ -19,6 +49,25 @@ namespace xrpl::NodeStore { +/** Decompress an LZ4-compressed blob produced by `lz4Compress`. + * + * Reads a leading varint that encodes the original uncompressed size, + * allocates exactly that many bytes via `bf`, then calls + * `LZ4_decompress_safe` into the allocated buffer. + * + * @tparam BufferFactory Callable with signature `void*(std::size_t n)` that + * allocates `n` bytes and returns a pointer to them. The codec does not + * free this memory; lifetime is governed by the caller. + * @param in Pointer to the compressed input buffer (varint prefix + LZ4 data). + * @param inSize Number of bytes at `in`. + * @param bf Factory used to allocate the decompressed output buffer. + * @return Pair of (pointer to decompressed data, decompressed byte count). + * The pointer is the buffer returned by `bf`. + * @throws std::runtime_error if `inSize` would overflow `int`, if the leading + * varint is missing or occupies the entire buffer, if the decompressed + * size would overflow `int`, or if `LZ4_decompress_safe` returns a byte + * count that does not match the expected output size. + */ template std::pair lz4Decompress(void const* in, std::size_t inSize, BufferFactory&& bf) @@ -48,6 +97,24 @@ lz4Decompress(void const* in, std::size_t inSize, BufferFactory&& bf) return {out, outSize}; } +/** Compress a raw blob using LZ4 and prepend the uncompressed size as a varint. + * + * Allocates a single output buffer via `bf` sized for the varint prefix plus + * `LZ4_compressBound(inSize)` bytes (worst-case LZ4 output), then writes the + * varint followed by the compressed payload. The returned size reflects the + * actual compressed size, not the worst-case bound. + * + * @tparam BufferFactory Callable with signature `void*(std::size_t n)` that + * allocates `n` bytes and returns a pointer to them. The codec does not + * free this memory; lifetime is governed by the caller. + * @param in Pointer to the uncompressed input data. + * @param inSize Number of bytes at `in`. + * @param bf Factory used to allocate the output buffer. + * @return Pair of (pointer to compressed output, compressed byte count + * including the varint prefix). The pointer is the buffer returned by `bf`. + * @throws std::runtime_error if `LZ4_compress_default` returns 0 (compression + * failure). + */ template std::pair lz4Compress(void const* in, std::size_t inSize, BufferFactory&& bf) @@ -69,17 +136,29 @@ lz4Compress(void const* in, std::size_t inSize, BufferFactory&& bf) return result; } -//------------------------------------------------------------------------------ - -/* - object types: - - 0 = Uncompressed - 1 = lz4 compressed - 2 = inner node compressed - 3 = full inner node -*/ - +/** Decompress a NodeStore blob encoded by `nodeobjectCompress`. + * + * Reads the leading varint type tag and dispatches to the appropriate decoder: + * - Type 0: uncompressed legacy data — returned as a non-owning view into `in`. + * - Type 1: delegates to `lz4Decompress`. + * - Type 2: sparse inner-node — reads a 16-bit bitmask, then reconstructs a + * 525-byte SHAMap inner-node blob with only the non-zero child hashes + * filled in and `index`/`unused`/`kind` fields zeroed. + * - Type 3: full inner-node — reads all 512 bytes of child hashes directly and + * reconstructs the 525-byte blob with metadata fields zeroed. + * + * @tparam BufferFactory Callable with signature `void*(std::size_t n)` that + * allocates `n` bytes and returns a pointer to them. Not invoked for type 0 + * (the returned pointer into `in` is valid only as long as `in` is alive). + * @param in Pointer to the encoded input buffer. + * @param inSize Number of bytes at `in`. + * @param bf Factory used to allocate decoded output for types 1–3. + * @return Pair of (pointer to decoded data, decoded byte count). + * @throws std::runtime_error if the type varint is missing, if any size check + * fails during inner-node reconstruction, or if the type tag is unrecognized. + * @note For type 0 the returned pointer aliases `in`; for types 1–3 it points + * into the buffer supplied by `bf`. + */ template std::pair nodeobjectDecompress(void const* in, std::size_t inSize, BufferFactory&& bf) @@ -184,6 +263,14 @@ nodeobjectDecompress(void const* in, std::size_t inSize, BufferFactory&& bf) return result; } +/** Return a pointer to a zero-initialized 32-byte static buffer. + * + * Used by `nodeobjectCompress` as a sentinel to detect empty child-hash + * slots in a SHAMap inner node via `memcmp`. The buffer is function-local + * static so it is initialized exactly once and lives for the process lifetime. + * + * @return Pointer to a 32-byte buffer whose contents are all zero bytes. + */ template void const* zero32() @@ -192,6 +279,31 @@ zero32() return kV.data(); } +/** Compress a raw NodeStore blob into the NodeStore on-disk wire format. + * + * Detects SHAMap inner nodes (exactly 525 bytes with `HashPrefix::InnerNode` + * at byte offset 9) and applies a specialized encoding: + * - Sparse (type 2): fewer than 16 child slots occupied — stores a 16-bit + * presence bitmask (bit 0x8000 = slot 0) followed by only the non-zero + * hashes packed contiguously. + * - Full (type 3): all 16 slots occupied — stores all 512 hash bytes directly, + * skipping the bitmask. + * + * All other blobs are LZ4-compressed (type 1) via `lz4Compress`. Type 0 + * (uncompressed) is never written; the `kCODEC_TYPE` constant is fixed at 1. + * + * @tparam BufferFactory Callable with signature `void*(std::size_t n)` that + * allocates `n` bytes and returns a pointer to them. The codec does not + * free this memory; lifetime is governed by the caller. + * @param in Pointer to the uncompressed NodeStore blob. + * @param inSize Number of bytes at `in`. + * @param bf Factory used to allocate the encoded output buffer. + * @return Pair of (pointer to encoded data, encoded byte count). + * @throws std::runtime_error if LZ4 compression fails. + * @note Inner-node reconstruction zeros `index`, `unused`, and `kind` fields, + * so those fields are not preserved across a compress/decompress round-trip. + * Call `filterInner` on the source blob before round-trip verification. + */ template std::pair nodeobjectCompress(void const* in, std::size_t inSize, BufferFactory&& bf) @@ -199,7 +311,6 @@ nodeobjectCompress(void const* in, std::size_t inSize, BufferFactory&& bf) using std::runtime_error; using namespace nudb::detail; - // Check for inner node v1 if (inSize == 525) { istream is(in, inSize); @@ -228,7 +339,6 @@ nodeobjectCompress(void const* in, std::size_t inSize, BufferFactory&& bf) std::pair result; if (n < 16) { - // 2 = v1 inner node compressed auto const type = 2U; auto const vs = sizeVarint(type); result.second = vs + field::size + // mask @@ -241,7 +351,6 @@ nodeobjectCompress(void const* in, std::size_t inSize, BufferFactory&& bf) write(os, vh.data(), n * 32); return result; } - // 3 = full v1 inner node auto const type = 3U; auto const vs = sizeVarint(type); result.second = vs + (n * 32); // hashes @@ -261,7 +370,6 @@ nodeobjectCompress(void const* in, std::size_t inSize, BufferFactory&& bf) std::pair result; switch (kCODEC_TYPE) { - // case 0 was uncompressed data; we always compress now. case 1: // lz4 { std::uint8_t* p = nullptr; @@ -280,17 +388,29 @@ nodeobjectCompress(void const* in, std::size_t inSize, BufferFactory&& bf) return result; } -// Modifies an inner node to erase the ledger -// sequence and type information so the codec -// verification can pass. -// +/** Normalize an inner-node blob in place before codec round-trip verification. + * + * `nodeobjectCompress` reconstructs inner nodes with `index`, `unused`, and + * `kind` zeroed (those fields are not stored on disk). Comparing a raw source + * blob against the decompressed output would therefore fail unless the source + * is first normalized by zeroing the same fields. This function performs that + * normalization in place. + * + * The function is a no-op for any blob that is not exactly 525 bytes or does + * not carry the `HashPrefix::InnerNode` marker at byte offset 9. + * + * @param in Pointer to the blob to normalize. Modified in place when the blob + * is identified as a SHAMap inner node. + * @param inSize Number of bytes at `in`. + * @note This function is used by the NodeStore import tool prior to calling + * `nodeobjectCompress` so that the verification `memcmp` succeeds. + */ template void filterInner(void* in, std::size_t inSize) { using namespace nudb::detail; - // Check for inner node if (inSize == 525) { istream is(in, inSize); diff --git a/include/xrpl/nodestore/detail/varint.h b/include/xrpl/nodestore/detail/varint.h index c98b36e322..fa3ce158e7 100644 --- a/include/xrpl/nodestore/detail/varint.h +++ b/include/xrpl/nodestore/detail/varint.h @@ -1,3 +1,23 @@ +/** @file + * Variable-length integer (varint) encoding for the NodeStore serialization + * layer. + * + * Provides a base-127 variant of the Protocol Buffers LEB128 varint format. + * Small values (0–126) occupy exactly one byte; larger values expand up to + * 10 bytes for a full 64-bit quantity. Used by `codec.h` for two purposes: + * the one-byte object-type discriminant prefix on every stored blob, and the + * decompressed-size prefix that precedes LZ4-compressed payloads. + * + * @note The encoding uses base-127, not the standard base-128, so the byte + * value `0x7F` never appears as a payload byte. The continuation flag + * remains bit 7 (`0x80`), matching the structural appearance of protobuf + * varints. + * + * @note All multi-definition functions (`readVarint`, `writeVarint`) are + * function templates with a defaulted `` parameter solely + * to satisfy the ODR when the header is included in multiple translation + * units. They carry no template behaviour beyond that. + */ #pragma once #include @@ -7,30 +27,60 @@ namespace xrpl::NodeStore { -// This is a variant of the base128 varint format from -// google protocol buffers: -// https://developers.google.com/protocol-buffers/docs/encoding#varints - -// field tag +/** Tag type used to select the varint overloads of `read` and `write`. + * + * Pass as the explicit template argument at call sites: + * @code + * read(is, u); + * write(os, type); + * @endcode + * The tag distinguishes these overloads from NuDB's built-in typed + * `read`/`write` functions for `uint8_t`, `uint16_t`, etc. + */ struct varint; -// Metafuncton to return largest -// possible size of T represented as varint. -// T must be unsigned +/** Compile-time upper bound on the encoded byte width of type `T`. + * + * `kMAX` is the maximum number of bytes that any value of unsigned type `T` + * can occupy when encoded as a base-127 varint. Use it to allocate + * stack-local buffers without dynamic allocation: + * @code + * std::array::kMAX> buf{}; + * @endcode + * + * @tparam T An unsigned integer type. Instantiation with a signed type is + * disabled via SFINAE. + */ template > struct varint_traits; +/** Specialisation enabled for unsigned types. */ template struct varint_traits { explicit varint_traits() = default; + /** Maximum encoded byte count for type `T` under base-127 encoding. */ static std::size_t constexpr kMAX = (8 * sizeof(T) + 6) / 7; }; -// Returns: Number of bytes consumed or 0 on error, -// if the buffer was too small or t overflowed. -// +/** Decode a base-127 varint from a raw byte buffer. + * + * Scans `buf` for continuation bytes (bit 7 set), then decodes using + * Horner's method from most-significant to least-significant byte so that + * `t = t * 127 + (byte & 0x7F)` reconstructs the original value. + * + * @param buf Pointer to the first byte of the encoded varint. + * @param buflen Number of bytes available in `buf`. + * @param t Output parameter set to the decoded value on success; + * unmodified on error. + * @return Number of bytes consumed from `buf`, or `0` on error. Error + * conditions: `buflen == 0`, the continuation chain extends past + * `buflen`, or arithmetic overflow during accumulation. + * @note The zero value is handled as a special case because the + * overflow guard (`t <= t0`) would otherwise trigger spuriously when + * `t` remains zero after processing a single zero byte. + */ template std::size_t readVarint(void const* buf, std::size_t buflen, std::size_t& t) @@ -67,6 +117,16 @@ readVarint(void const* buf, std::size_t buflen, std::size_t& t) return used; } +/** Compute the encoded byte width of `v` without writing anything. + * + * Mirrors the byte count that `writeVarint` would return for the same value. + * Use this to pre-compute output buffer sizes before encoding. + * + * @tparam T An unsigned integer type. + * @param v The value whose encoded size is needed. + * @return Number of bytes required to encode `v` as a base-127 varint + * (always >= 1). + */ template >* = nullptr> std::size_t sizeVarint(T v) @@ -80,6 +140,17 @@ sizeVarint(T v) return n; } +/** Encode `v` into the buffer at `p0` as a base-127 varint. + * + * Writes bytes in least-significant-first order. Each byte carries a 7-bit + * payload in bits 0–6 (range 0–126); bit 7 is set on all bytes except the + * last, signalling that more bytes follow. + * + * @param p0 Destination buffer. Must have capacity of at least + * `sizeVarint(v)` bytes; no bounds check is performed. + * @param v The value to encode. + * @return Number of bytes written (same as `sizeVarint(v)`). + */ template std::size_t writeVarint(void* p0, std::size_t v) @@ -97,8 +168,17 @@ writeVarint(void* p0, std::size_t v) return p - reinterpret_cast(p0); } -// input stream - +/** Read a varint from a NuDB input stream into `u`. + * + * Advances the stream one byte at a time until a byte without the + * continuation bit is consumed, then delegates to `readVarint` over the + * accumulated span. + * + * @tparam T Must be `varint`; the tag selects this overload over NuDB's + * built-in typed `read` functions. + * @param is The NuDB input stream to read from. + * @param u Output parameter set to the decoded value. + */ template >* = nullptr> void read(nudb::detail::istream& is, std::size_t& u) @@ -110,8 +190,16 @@ read(nudb::detail::istream& is, std::size_t& u) readVarint(p0, p1 - p0, u); } -// output stream - +/** Write `t` as a varint into a NuDB output stream. + * + * Reserves exactly `sizeVarint(t)` bytes in the stream and encodes `t` + * directly into that region via `writeVarint`. + * + * @tparam T Must be `varint`; the tag selects this overload over NuDB's + * built-in typed `write` functions. + * @param os The NuDB output stream to write into. + * @param t The value to encode. + */ template >* = nullptr> void write(nudb::detail::ostream& os, std::size_t t) diff --git a/include/xrpl/protocol/AMMCore.h b/include/xrpl/protocol/AMMCore.h index 9d8f8c62b0..07c1bc386b 100644 --- a/include/xrpl/protocol/AMMCore.h +++ b/include/xrpl/protocol/AMMCore.h @@ -1,3 +1,13 @@ +/** @file + * Protocol-level constants, LP-token identity derivation, input-validation + * helpers, and fee-conversion utilities for the XRP Ledger Automated Market + * Maker (AMM) feature. + * + * Every AMM transactor (`AMMCreate`, `AMMDeposit`, `AMMWithdraw`, `AMMBid`, + * `AMMVote`) and `AMMHelpers.h` depend on this header as the single + * authoritative source for numeric parameter encoding and preflight checks. + */ + #pragma once #include @@ -8,40 +18,116 @@ namespace xrpl { +/** Maximum trading fee, in tenths of a basis point. + * + * Fee integers are in the range `[0, kTRADING_FEE_THRESHOLD]` where + * 1 unit = 0.001% (1/10 bps) and 1000 = 1%. + */ std::uint16_t constexpr kTRADING_FEE_THRESHOLD = 1000; // 1% -// Auction slot +// --- Auction slot parameters --- + +/** Duration of a single auction slot window, in seconds (24 hours). */ std::uint32_t constexpr kTOTAL_TIME_SLOT_SECS = 24 * 3600; + +/** Number of equal time intervals the 24-hour auction window is divided into. + * + * The slot index (0–19) determines how much of the bid price is refunded to + * the outgoing holder when a new bidder takes over mid-window. + */ std::uint16_t constexpr kAUCTION_SLOT_TIME_INTERVALS = 20; + +/** Maximum number of additional accounts a slot holder may authorise to trade + * at the discounted fee. + */ std::uint16_t constexpr kAUCTION_SLOT_MAX_AUTH_ACCOUNTS = 4; + +/** Divisor used to convert a fee integer to the fee fraction `f`. + * + * `f = tfee / kAUCTION_SLOT_FEE_SCALE_FACTOR`. Chosen so that + * `kTRADING_FEE_THRESHOLD / kAUCTION_SLOT_FEE_SCALE_FACTOR == 0.01` (1%). + */ std::uint32_t constexpr kAUCTION_SLOT_FEE_SCALE_FACTOR = 100000; + +/** Denominator for the slot holder's discounted fee. + * + * The effective fee for a slot holder is `tradingFee / kAUCTION_SLOT_DISCOUNTED_FEE_FRACTION`. + */ std::uint32_t constexpr kAUCTION_SLOT_DISCOUNTED_FEE_FRACTION = 10; + +/** Denominator used to compute the minimum bid price for the auction slot. + * + * Minimum bid = `lptAMMBalance × tradingFee / kAUCTION_SLOT_MIN_FEE_FRACTION`. + */ std::uint32_t constexpr kAUCTION_SLOT_MIN_FEE_FRACTION = 25; + +/** Duration of one auction slot interval, in seconds (72 minutes). + * + * Derived as `kTOTAL_TIME_SLOT_SECS / kAUCTION_SLOT_TIME_INTERVALS`. + */ std::uint32_t constexpr kAUCTION_SLOT_INTERVAL_DURATION = kTOTAL_TIME_SLOT_SECS / kAUCTION_SLOT_TIME_INTERVALS; -// Votes +// --- Fee-governance vote parameters --- + +/** Maximum number of simultaneous fee-vote records in an AMM object. */ std::uint16_t constexpr kVOTE_MAX_SLOTS = 8; + +/** Scale factor for LP vote weights. + * + * Each LP's proportional vote weight is stored as an integer in + * `[0, kVOTE_WEIGHT_SCALE_FACTOR]`, avoiding division until the + * weighted-average fee is computed. + */ std::uint32_t constexpr kVOTE_WEIGHT_SCALE_FACTOR = 100000; class STObject; class STAmount; class Rules; -/** Calculate Liquidity Provider Token (LPT) Currency. +/** Derive the deterministic LP token `Currency` code for an asset pair. + * + * The two assets are sorted canonically before hashing, so + * `ammLPTCurrency(a, b) == ammLPTCurrency(b, a)` for any asset pair. + * The resulting 20-byte currency has `0x03` as its first byte (the AMM + * currency sentinel), followed by 19 bytes taken from + * `sha512Half(canonicalId(min), canonicalId(max))`. For IOU/XRP assets the + * canonical identifier is the `Currency` field; for MPT assets it is the + * `MPTID`. + * + * @param asset1 One of the two pool assets. + * @param asset2 The other pool asset. + * @return A `Currency` value that uniquely identifies the LP token for this + * pair on the ledger and is distinct from any normal IOU or XRP currency. */ Currency ammLPTCurrency(Asset const& asset1, Asset const& asset2); -/** Calculate LPT Issue from AMM asset pair. +/** Construct the full LP token `Issue` (currency + issuer) for an asset pair. + * + * Combines the deterministic currency from `ammLPTCurrency` with the AMM + * account's `AccountID` to produce the `Issue` that `STAmount` operations + * require. + * + * @param asset1 One of the two pool assets. + * @param asset2 The other pool asset. + * @param ammAccountID The `AccountID` of the AMM ledger object. + * @return An `Issue` identifying the LP token for this AMM pool. */ Issue ammLPTIssue(Asset const& asset1, Asset const& asset2, AccountID const& ammAccountID); -/** Validate the amount. - * If validZero is false and amount is beast::zero then invalid amount. - * Return error code if invalid amount. - * If pair then validate amount's issue matches one of the pair's issue. +/** Validate an `STAmount` for use in an AMM transaction (preflight check). + * + * Delegates asset-level validation to `invalidAMMAsset`, then additionally + * rejects negative values and, unless `validZero` is true, zero values. + * + * @param amount The amount to validate. + * @param pair When provided, the amount's asset must match one of the + * two assets in the pair; otherwise `temBAD_AMM_TOKENS` is returned. + * @param validZero If `false` (the default), a zero amount is rejected with + * `temBAD_AMOUNT`. + * @return `tesSUCCESS` if valid; a `tem*` error code otherwise. */ NotTEC invalidAMMAmount( @@ -49,30 +135,81 @@ invalidAMMAmount( std::optional> const& pair = std::nullopt, bool validZero = false); +/** Validate a single asset for use in an AMM transaction (preflight check). + * + * - MPT assets with a zero issuer → `temBAD_MPT`. + * - XRP with a non-zero issuer → `temBAD_ISSUER`. + * - Malformed currency codes → `temBAD_CURRENCY`. + * - Asset not matching either element of `pair` (when provided) → `temBAD_AMM_TOKENS`. + * + * @param asset The asset to validate. + * @param pair When provided, `asset` must equal `pair->first` or + * `pair->second`; used to confirm the asset belongs to a specific pool. + * @return `tesSUCCESS` if valid; a `tem*` error code otherwise. + */ NotTEC invalidAMMAsset( Asset const& asset, std::optional> const& pair = std::nullopt); +/** Validate a pair of assets for use in an AMM transaction (preflight check). + * + * Rejects identical assets (`temBAD_AMM_TOKENS`) before delegating each + * asset to `invalidAMMAsset`. + * + * @param asset1 First asset of the pair. + * @param asset2 Second asset of the pair. + * @param pair When provided, each asset must match one element of this + * known-good pair; passed through to `invalidAMMAsset`. + * @return `tesSUCCESS` if valid; a `tem*` error code otherwise. + */ NotTEC invalidAMMAssetPair( Asset const& asset1, Asset const& asset2, std::optional> const& pair = std::nullopt); -/** Get time slot of the auction slot. +/** Compute the zero-based time-slot index for an active auction slot. + * + * Derives the slot start from `auctionSlot[sfExpiration] - kTOTAL_TIME_SLOT_SECS`, + * then integer-divides elapsed seconds by `kAUCTION_SLOT_INTERVAL_DURATION`. + * Returns `std::nullopt` when `current` is before the slot start or at or + * after `sfExpiration`, indicating the slot has expired or has not yet begun. + * + * @param current Current ledger time (NetClock seconds). + * @param auctionSlot The `STObject` representing the AMM's auction slot; + * must contain `sfExpiration`. + * @return Slot index in `[0, kAUCTION_SLOT_TIME_INTERVALS)`, or + * `std::nullopt` if the slot is not currently active. + * @note An `XRPL_ASSERT` fires if `sfExpiration < kTOTAL_TIME_SLOT_SECS`, + * which is considered an impossible ledger state. */ std::optional ammAuctionTimeSlot(std::uint64_t current, STObject const& auctionSlot); -/** Return true if required AMM amendments are enabled +/** Return true if the network has enabled both AMM amendments. + * + * Requires both `featureAMM` and `fixUniversalNumber`. The second + * amendment is a hard dependency: AMM arithmetic relies on the corrected + * high-precision numeric library introduced by `fixUniversalNumber`, and + * allowing AMM transactions on networks without it would cause overflow or + * precision loss in intermediate swap calculations. + * + * @param rules Snapshot of currently enabled amendments. + * @return `true` only when both `featureAMM` and `fixUniversalNumber` are + * active. */ bool ammEnabled(Rules const&); -/** Convert to the fee from the basis points - * @param tfee trading fee in {0, 1000} - * 1 = 1/10bps or 0.001%, 1000 = 1% +/** Convert a trading fee integer to the fee fraction `f`. + * + * Divides `tfee` by `kAUCTION_SLOT_FEE_SCALE_FACTOR` (100,000) to produce + * the dimensionless fraction used in swap arithmetic. At the maximum fee + * `kTRADING_FEE_THRESHOLD = 1000`, `getFee` returns `0.01` (1%). + * + * @param tfee Trading fee integer in `[0, kTRADING_FEE_THRESHOLD]`. + * @return Fee fraction `f = tfee / 100000`. */ inline Number getFee(std::uint16_t tfee) @@ -80,8 +217,15 @@ getFee(std::uint16_t tfee) return Number{tfee} / kAUCTION_SLOT_FEE_SCALE_FACTOR; } -/** Get fee multiplier (1 - tfee) - * @tfee trading fee in basis points +/** Compute the full-fee swap multiplier `(1 - f)`. + * + * Applied to the input amount when the complete trading fee is charged, + * i.e. during ordinary swaps. In `AMMDeposit`, this is the `f1` factor + * in the single-asset constant-product formula. + * + * @param tfee Trading fee integer in `[0, kTRADING_FEE_THRESHOLD]`. + * @return `1 - getFee(tfee)`. + * @see feeMultHalf */ inline Number feeMult(std::uint16_t tfee) @@ -89,8 +233,15 @@ feeMult(std::uint16_t tfee) return 1 - getFee(tfee); } -/** Get fee multiplier (1 - tfee / 2) - * @tfee trading fee in basis points +/** Compute the half-fee swap multiplier `(1 - f/2)`. + * + * Used during single-asset deposits where only half the implied fee is + * deducted. In `AMMDeposit`, the combined factor is + * `f2 = feeMultHalf(tfee) / feeMult(tfee)`. + * + * @param tfee Trading fee integer in `[0, kTRADING_FEE_THRESHOLD]`. + * @return `1 - getFee(tfee) / 2`. + * @see feeMult */ inline Number feeMultHalf(std::uint16_t tfee) diff --git a/include/xrpl/protocol/AccountID.h b/include/xrpl/protocol/AccountID.h index 0b15f651bc..7dc18dcd85 100644 --- a/include/xrpl/protocol/AccountID.h +++ b/include/xrpl/protocol/AccountID.h @@ -1,3 +1,8 @@ +/** @file + * Defines the AccountID type, serialization helpers, sentinel constants, + * and the optional base58 encoding cache for XRP Ledger account identities. + */ + #pragma once #include @@ -16,6 +21,12 @@ namespace xrpl { namespace detail { +/** Phantom tag type that makes AccountID a distinct strong type. + * + * Passed as the second template argument to `BaseUInt<160, Tag>` so that + * a 160-bit account hash cannot be silently used where a raw hash or node ID + * is expected, and vice versa. The class has no data members or behaviour. + */ class AccountIDTag { public: @@ -24,47 +35,109 @@ public: } // namespace detail -/** A 160-bit unsigned that uniquely identifies an account. */ +/** A 160-bit identifier that uniquely addresses an XRP Ledger account. + * + * Stored as five `uint32_t` values in big-endian byte order — a layout + * that is part of the binary serialization protocol and cannot be changed. + * Derived from a public key via SHA-256 + RIPEMD-160 (`calcAccountID()`). + * + * The phantom tag `detail::AccountIDTag` makes this a distinct C++ type, + * preventing accidental mixing with other 160-bit quantities at compile time. + * + * @see calcAccountID(), toBase58(), parseBase58() + */ using AccountID = BaseUInt<160, detail::AccountIDTag>; -/** Convert AccountID to base58 checked string */ +/** Encode an AccountID as a Base58Check string. + * + * Prepends `TokenType::AccountID` (value 0) before encoding. When the + * global cache has been initialised via `initAccountIdCache()`, the result + * is served from the cache to avoid repeated SHA-256 checksum computation. + * + * @param v The account identifier to encode. + * @return The Base58Check-encoded string (always 25–34 printable characters). + * @see initAccountIdCache(), parseBase58() + */ std::string toBase58(AccountID const& v); -/** Parse AccountID from checked, base58 string. - @return std::nullopt if a parse error occurs -*/ +/** Decode a Base58Check string into an AccountID. + * + * Validates the `TokenType::AccountID` prefix and requires the decoded + * payload to be exactly 20 bytes. Input that fails either check returns + * `std::nullopt` rather than throwing, because external input is frequently + * untrusted. + * + * @param s The Base58Check-encoded account string to parse. + * @return The decoded AccountID, or `std::nullopt` on any parse failure. + * @see toBase58() + */ template <> std::optional parseBase58(std::string const& s); -/** Compute AccountID from public key. - - The account ID is computed as the 160-bit hash of the - public key data. This excludes the version byte and - guard bytes included in the base58 representation. - -*/ +/** Compute the AccountID for a public key using SHA-256 + RIPEMD-160. + * + * Applies `RipeshaHasher` to the raw public-key bytes (no version byte). + * The double-hash matches Bitcoin's derivation: SHA-256 prevents + * length-extension attacks, and RIPEMD-160 is considered safe at 160 bits. + * XRPL adopted the scheme to avoid any claim of weaker security relative + * to Bitcoin. + * + * @note Declaration lives in `PublicKey.h`; the implementation is in + * `AccountID.cpp`. + */ // VFALCO In PublicKey.h for now // AccountID // calcAccountID (PublicKey const& pk); -/** A special account that's used as the "issuer" for XRP. */ +/** Return the canonical XRP issuer sentinel: the all-zero AccountID. + * + * Used as the issuer field in XRP `STAmount` values. Code that needs to + * test whether an amount is native XRP should prefer checking the native + * flag or the currency directly rather than comparing the issuer against + * this value — see the deprecated `isXRP(AccountID)` overload. + * + * @return A function-local static `AccountID` equal to `beast::kZERO`. + * Returned by `const&` to avoid copies; lifetime is the process lifetime. + */ AccountID const& xrpAccount(); -/** A placeholder for empty accounts. */ +/** Return the "no account" sentinel: `AccountID(1)`. + * + * Used as a placeholder in offer and trust-line fields that have no + * meaningful account value (e.g., an uninitialized or absent counterparty). + * Distinct from `xrpAccount()` (all zeros) so the two sentinels cannot + * be confused. + * + * @return A function-local static `AccountID` with value 1. + * Returned by `const&` to avoid copies; lifetime is the process lifetime. + */ AccountID const& noAccount(); -/** Convert hex or base58 string to AccountID. - - @return `true` if the parsing was successful. -*/ +/** Parse a hex or Base58Check string into an AccountID. + * + * Tries hex first (`parseHex`), then falls back to Base58Check. Used + * in legacy configuration parsing where the encoding is not guaranteed. + * + * @param issuer Output: receives the parsed AccountID on success. + * @param s The hex (40 chars) or Base58Check string to parse. + * @return `true` if parsing succeeded and `issuer` was written. + * @deprecated Prefer `parseBase58()` for user-facing input. + */ // DEPRECATED bool toIssuer(AccountID&, std::string const&); +/** Test whether an AccountID equals the XRP issuer sentinel (all zeros). + * + * @param c The account identifier to test. + * @return `true` if `c` equals `beast::kZERO` (i.e., equals `xrpAccount()`). + * @deprecated Check the currency field or the native/integral flag instead; + * relying on the zero-account-as-issuer convention is a leaky abstraction. + */ // DEPRECATED Should be checking the currency or native flag inline bool isXRP(AccountID const& c) @@ -72,6 +145,12 @@ isXRP(AccountID const& c) return c == beast::kZERO; } +/** Convert an AccountID to its Base58Check string representation. + * + * @param account The account identifier to convert. + * @return The Base58Check-encoded string. + * @deprecated Use `toBase58()` directly. + */ // DEPRECATED inline std::string to_string(AccountID const& account) @@ -79,6 +158,14 @@ to_string(AccountID const& account) return toBase58(account); } +/** Write the Base58Check encoding of an AccountID to an output stream. + * + * @param os The stream to write to. + * @param x The account identifier to encode. + * @return `os`, to allow chaining. + * @deprecated Prefer explicit `toBase58()` calls; stream output silently + * invokes Base58 encoding and can be surprising in logging contexts. + */ // DEPRECATED inline std::ostream& operator<<(std::ostream& os, AccountID const& x) @@ -87,17 +174,22 @@ operator<<(std::ostream& os, AccountID const& x) return os; } -/** Initialize the global cache used to map AccountID to base58 conversions. - - The cache is optional and need not be initialized. But because conversion - is expensive (it requires a SHA-256 operation) in most cases the overhead - of the cache is worth the benefit. - - @param count The number of entries the cache should accommodate. Zero will - disable the cache, releasing any memory associated with it. - - @note The function will only initialize the cache the first time it is - invoked. Subsequent invocations do nothing. +/** Initialize the global AccountID → Base58Check encoding cache. + * + * Base58Check encoding requires a SHA-256 checksum on every call, which is + * expensive at transaction-processing throughput. The cache uses a + * direct-mapped open-addressing table with 64 spinlocks packed into a single + * `atomic` (via `PackedSpinlock`) to allow concurrent access with + * minimal memory overhead. The index hash is `hardened_hash<>` (DoS- + * resistant seeded hash) to prevent crafted workloads from degrading lookups. + * + * The cache is strictly optional: if never initialised, `toBase58()` falls + * through to `encodeBase58Token` on every call. + * + * @param count The number of cache slots to allocate. Pass 0 to leave the + * cache disabled (no-op if already disabled). + * @note This function initialises the cache at most once. Subsequent calls + * with any `count` value are silently ignored. */ void initAccountIdCache(std::size_t count); @@ -106,6 +198,21 @@ initAccountIdCache(std::size_t count); //------------------------------------------------------------------------------ namespace json { + +/** Extract and parse an AccountID from a JSON object field. + * + * Reads `field` from `v` as a string, then decodes it as a Base58Check + * account address. Throws `JsonTypeMismatchError` if the field is absent, + * not a string, or not a valid AccountID encoding — the same error type + * raised for any other JSON type mismatch, enabling uniform error handling + * in RPC and transaction-parsing code. + * + * @param v The JSON object to read from. + * @param field The SField identifying the key to look up. + * @return The decoded AccountID. + * @throws JsonTypeMismatchError if the field is missing, not a string, or + * cannot be decoded as a valid Base58Check AccountID. + */ template <> inline xrpl::AccountID getOrThrow(json::Value const& v, xrpl::SField const& field) @@ -123,6 +230,15 @@ getOrThrow(json::Value const& v, xrpl::SField const& field) namespace std { +/** `std::hash` specialization for AccountID, delegating to `hardened_hash<>`. + * + * Maintains compatibility with standard-library unordered containers that + * key on `AccountID`. The underlying hasher uses a random seed (DoS- + * resistant), so hash values differ across process restarts. + * + * @deprecated Prefer `beast::uhash` or XRPL's hardened unordered containers + * (`UnorderedMap`, `UnorderedSet`) for new code. + */ // DEPRECATED // VFALCO Use beast::uhash or a hardened container template <> diff --git a/include/xrpl/protocol/AmountConversions.h b/include/xrpl/protocol/AmountConversions.h index 53b7dace3c..dc0ae988ec 100644 --- a/include/xrpl/protocol/AmountConversions.h +++ b/include/xrpl/protocol/AmountConversions.h @@ -1,5 +1,17 @@ #pragma once +/** @file + * Conversion utilities between the four XRPL amount representations. + * + * The protocol defines four amount types, each optimized for a different + * concern: `XRPAmount` (integer drops), `MPTAmount` (integer MPT units), + * `IOUAmount` (normalized floating-point), and `STAmount` (wire-level union + * over all three). Generic algorithms — AMM pricing, pathfinding, offer + * crossing — need to work across all four without duplicating logic. This + * header provides the glue: inline conversion functions that move freely + * between representations. No arithmetic or business logic lives here. + */ + #include #include #include @@ -9,6 +21,20 @@ namespace xrpl { +/** Wrap an `IOUAmount` in a serializable `STAmount` tagged with the given asset. + * + * `IOUAmount` stores a signed mantissa; `STAmount` stores an unsigned mantissa + * with a separate sign bit. This overload performs that split manually and + * constructs via `STAmount::Unchecked()` to skip re-canonicalization — + * `IOUAmount` is already normalized so re-canonicalizing would be wasted work. + * + * @param iou The IOU value to wrap. + * @param asset The asset identity to embed; must hold an `Issue` (not XRP + * or MPT) — verified by assertion. + * @return An `STAmount` encoding the same value and sign as `iou`. + * @note Passing an XRP or MPT asset silently produces wrong data in release + * builds; the assertion catches this only in debug builds. + */ inline STAmount toSTAmount(IOUAmount const& iou, Asset const& asset) { @@ -18,12 +44,26 @@ toSTAmount(IOUAmount const& iou, Asset const& asset) return STAmount(asset, umant, iou.exponent(), isNeg, STAmount::Unchecked()); } +/** Wrap an `IOUAmount` in an `STAmount` with a placeholder `noIssue()` asset. + * + * Convenience overload for contexts where the true asset identity is not + * available at the call site. The resulting `STAmount` carries `noIssue()` + * as its asset tag and should not be used in wire serialization. + * + * @param iou The IOU value to wrap. + * @return An `STAmount` encoding the value of `iou` with `noIssue()` asset. + */ inline STAmount toSTAmount(IOUAmount const& iou) { return toSTAmount(iou, noIssue()); } +/** Wrap an `XRPAmount` in a serializable `STAmount`. + * + * @param xrp The XRP drop count to wrap; may be negative. + * @return A native `STAmount` encoding the same value and sign as `xrp`. + */ inline STAmount toSTAmount(XRPAmount const& xrp) { @@ -32,6 +72,16 @@ toSTAmount(XRPAmount const& xrp) return STAmount(umant, isNeg); } +/** Wrap an `XRPAmount` in an `STAmount` given an explicit `Asset`. + * + * Exists to give generic code a uniform `toSTAmount(amount, asset)` call + * signature; delegates immediately to the asset-less overload after asserting + * that `asset` is XRP. + * + * @param xrp The XRP drop count to wrap. + * @param asset Must be the XRP asset — verified by assertion. + * @return A native `STAmount` encoding the same value as `xrp`. + */ inline STAmount toSTAmount(XRPAmount const& xrp, Asset const& asset) { @@ -39,12 +89,25 @@ toSTAmount(XRPAmount const& xrp, Asset const& asset) return toSTAmount(xrp); } +/** Wrap an `MPTAmount` in an `STAmount` with a placeholder `noMPT()` asset. + * + * @param mpt The MPT unit count to wrap. + * @return An `STAmount` encoding the value of `mpt` with `noMPT()` asset. + */ inline STAmount toSTAmount(MPTAmount const& mpt) { return STAmount(mpt, noMPT()); } +/** Wrap an `MPTAmount` in an `STAmount` tagged with the given MPT asset. + * + * @param mpt The MPT unit count to wrap. + * @param asset The asset identity to embed; must hold an `MPTIssue` — + * verified by assertion. + * @return An `STAmount` encoding the value of `mpt` with the given + * `MPTIssue` identity. + */ inline STAmount toSTAmount(MPTAmount const& mpt, Asset const& asset) { @@ -52,10 +115,24 @@ toSTAmount(MPTAmount const& mpt, Asset const& asset) return STAmount(mpt, asset.get()); } +/** Primary template for `STAmount` → lean-type extraction; intentionally deleted. + * + * Calling `toAmount(stamt)` with an unsupported `T` is a hard compile + * error rather than a linker error or silent mis-conversion. Only the + * explicit specializations below (`STAmount`, `IOUAmount`, `XRPAmount`, + * `MPTAmount`) are valid. + * + * @tparam T Target amount type. + */ template T toAmount(STAmount const& amt) = delete; +/** Identity conversion: return the `STAmount` unchanged. + * + * @param amt The `STAmount` to return. + * @return `amt` unchanged. + */ template <> inline STAmount toAmount(STAmount const& amt) @@ -63,6 +140,16 @@ toAmount(STAmount const& amt) return amt; } +/** Extract the IOU value from an `STAmount` as an `IOUAmount`. + * + * Reconstitutes the signed mantissa (STAmount stores it unsigned + sign bit) + * and constructs an `IOUAmount` directly without re-canonicalization. + * + * @param amt The source `STAmount`; must not be a native XRP amount — + * verified by assertion. Mantissa must fit in `int64_t` — verified + * by assertion. + * @return An `IOUAmount` with the same signed mantissa and exponent. + */ template <> inline IOUAmount toAmount(STAmount const& amt) @@ -77,6 +164,13 @@ toAmount(STAmount const& amt) return IOUAmount(sMant, amt.exponent()); } +/** Extract the XRP drop count from an `STAmount` as an `XRPAmount`. + * + * @param amt The source `STAmount`; must be a native XRP amount — + * verified by assertion. Mantissa must fit in `int64_t` — verified + * by assertion. + * @return An `XRPAmount` holding the signed drop count. + */ template <> inline XRPAmount toAmount(STAmount const& amt) @@ -91,6 +185,20 @@ toAmount(STAmount const& amt) return XRPAmount(sMant); } +/** Extract the MPT unit count from an `STAmount` as an `MPTAmount`. + * + * MPT amounts are integers: the exponent must be exactly 0 and the + * mantissa must not exceed `kMAX_MP_TOKEN_AMOUNT`. Both constraints are + * checked in debug builds (assertion) and in release builds (exception), + * because a violation indicates data corruption or a ledger encoding bug + * that should surface loudly rather than silently truncate. + * + * @param amt The source `STAmount`; must hold an `MPTIssue`, have exponent + * 0, and mantissa ≤ `kMAX_MP_TOKEN_AMOUNT`. + * @return An `MPTAmount` holding the signed unit count. + * @throws std::runtime_error if `amt.exponent() != 0` or + * `amt.mantissa() > kMAX_MP_TOKEN_AMOUNT`. + */ template <> inline MPTAmount toAmount(STAmount const& amt) @@ -106,10 +214,24 @@ toAmount(STAmount const& amt) return MPTAmount(sMant); } +/** Primary template for `IOUAmount` → same-type extraction; intentionally deleted. + * + * Only the `IOUAmount` identity specialization below is valid. + * + * @tparam T Target amount type. + */ template T toAmount(IOUAmount const& amt) = delete; +/** Identity conversion: return the `IOUAmount` unchanged. + * + * Allows generic code to call `toAmount(iouValue)` without + * branching on whether the source is already the target type. + * + * @param amt The `IOUAmount` to return. + * @return `amt` unchanged. + */ template <> inline IOUAmount toAmount(IOUAmount const& amt) @@ -117,10 +239,24 @@ toAmount(IOUAmount const& amt) return amt; } +/** Primary template for `XRPAmount` → same-type extraction; intentionally deleted. + * + * Only the `XRPAmount` identity specialization below is valid. + * + * @tparam T Target amount type. + */ template T toAmount(XRPAmount const& amt) = delete; +/** Identity conversion: return the `XRPAmount` unchanged. + * + * Allows generic code to call `toAmount(xrpValue)` without + * branching on whether the source is already the target type. + * + * @param amt The `XRPAmount` to return. + * @return `amt` unchanged. + */ template <> inline XRPAmount toAmount(XRPAmount const& amt) @@ -128,10 +264,24 @@ toAmount(XRPAmount const& amt) return amt; } +/** Primary template for `MPTAmount` → same-type extraction; intentionally deleted. + * + * Only the `MPTAmount` identity specialization below is valid. + * + * @tparam T Target amount type. + */ template T toAmount(MPTAmount const& amt) = delete; +/** Identity conversion: return the `MPTAmount` unchanged. + * + * Allows generic code to call `toAmount(mptValue)` without + * branching on whether the source is already the target type. + * + * @param amt The `MPTAmount` to return. + * @return `amt` unchanged. + */ template <> inline MPTAmount toAmount(MPTAmount const& amt) @@ -139,6 +289,27 @@ toAmount(MPTAmount const& amt) return amt; } +/** Convert a `Number` intermediate result to a typed amount, applying a + * caller-specified rounding mode for XRP. + * + * Used by AMM pricing and pathfinding after performing arithmetic in + * `Number` space. The rounding mode override is applied **only for XRP**: + * XRP is an integer count of drops, so converting a rational intermediate + * requires a deterministic rounding decision. IOU and MPT types handle + * normalization internally and do not require external rounding control. + * `SaveNumberRoundMode` restores the previous thread-local rounding mode + * on destruction, even if the conversion throws. + * + * @tparam T Target amount type: `IOUAmount`, `XRPAmount`, `MPTAmount`, or + * `STAmount`. Any other type is a compile error. + * @param asset The asset identity for the result; must be consistent with + * `T` (e.g., XRP asset with `XRPAmount`). + * @param n The intermediate `Number` value to convert. + * @param mode Rounding mode applied when `T` is `XRPAmount` or when + * `T` is `STAmount` with an XRP asset. Defaults to the current + * thread-local rounding mode. + * @return The converted amount of type `T`. + */ template T toAmount(Asset const& asset, Number const& n, Number::RoundingMode mode = Number::getround()) @@ -172,6 +343,17 @@ toAmount(Asset const& asset, Number const& n, Number::RoundingMode mode = Number } } +/** Return the maximum representable value for a given amount type and asset. + * + * Dispatches at compile time on `T`. For `STAmount` the result depends on + * the runtime asset: XRP uses `kMAX_NATIVE_N` drops; IOU uses + * `(kMAX_VALUE, kMAX_OFFSET)`; MPT uses `kMAX_MP_TOKEN_AMOUNT`. + * + * @tparam T Target amount type: `IOUAmount`, `XRPAmount`, `MPTAmount`, or + * `STAmount`. Any other type is a compile error. + * @param asset The asset identity; consulted only when `T` is `STAmount`. + * @return The maximum representable value of type `T`. + */ template T toMaxAmount(Asset const& asset) @@ -205,12 +387,38 @@ toMaxAmount(Asset const& asset) } } +/** Convert a `Number` intermediate to an `STAmount` with a given asset and rounding mode. + * + * Thin wrapper around `toAmount(asset, n, mode)` provided so + * callers that always work with `STAmount` can use a non-template name. + * + * @param asset The asset identity for the result. + * @param n The intermediate `Number` value to convert. + * @param mode Rounding mode applied when `asset` is XRP. Defaults to the + * current thread-local rounding mode. + * @return An `STAmount` encoding `n` tagged with `asset`. + * @see toAmount + */ inline STAmount toSTAmount(Asset const& asset, Number const& n, Number::RoundingMode mode = Number::getround()) { return toAmount(asset, n, mode); } +/** Return a placeholder `Asset` for a given amount type. + * + * For `STAmount` this delegates to `amt.asset()` and returns the true asset. + * For lean types — `IOUAmount`, `XRPAmount`, `MPTAmount` — which do not + * carry asset identity, a sentinel is returned: `noIssue()`, `xrpIssue()`, + * or `noMPT()` respectively. Callers such as AMM helpers use this to + * produce an `Asset` argument for a subsequent `toAmount(asset, ...)` call + * where the true asset is known from context. + * + * @tparam T Source amount type. Any other type is a compile error. + * @param amt The amount whose asset identity is requested. + * @return The true `Asset` for `STAmount`; a type-appropriate sentinel + * for lean types. + */ template Asset getAsset(T const& amt) @@ -238,6 +446,19 @@ getAsset(T const& amt) } } +/** Extract a typed value from an `STAmount` by delegating to the + * appropriate accessor. + * + * Dispatches at compile time: `IOUAmount` → `a.iou()`, `XRPAmount` → + * `a.xrp()`, `MPTAmount` → `a.mpt()`, `STAmount` → identity. The + * `static_assert` in the else branch uses a type-dependent expression + * so it fires only when the unsupported branch is actually instantiated, + * not on every parse of the template. + * + * @tparam T Target lean type or `STAmount`. Any other type is a compile error. + * @param a The source `STAmount`. + * @return The value of `a` expressed as type `T`. + */ template constexpr T get(STAmount const& a) diff --git a/include/xrpl/protocol/ApiVersion.h b/include/xrpl/protocol/ApiVersion.h index 4d68cdaa5a..b31ca3db75 100644 --- a/include/xrpl/protocol/ApiVersion.h +++ b/include/xrpl/protocol/ApiVersion.h @@ -8,43 +8,76 @@ #include #include -namespace xrpl { - /** - * API version numbers used in later API versions + * @file ApiVersion.h + * @brief Single source of truth for the XRPL RPC API versioning scheme. * - * Requests with a version number in the range - * [apiMinimumSupportedVersion, apiMaximumSupportedVersion] - * are supported. + * Defines the compile-time integer constants that bound the accepted API + * version range, JSON parsing and serialization helpers that enforce those + * bounds at the RPC ingress point, and compile-time iteration templates + * (`forApiVersions`, `forAllApiVersions`) that let the rest of the codebase + * generate version-aware code paths without runtime switches. * - * If [beta_rpc_api] is enabled in config, the version numbers - * in the range [apiMinimumSupportedVersion, apiBetaVersion] - * are supported. - * - * Network Requests without explicit version numbers use - * apiVersionIfUnspecified. apiVersionIfUnspecified is 1, - * because all the RPC requests with a version >= 2 must - * explicitly specify the version in the requests. - * Note that apiVersionIfUnspecified will be lower than - * apiMinimumSupportedVersion when we stop supporting API - * version 1. - * - * Command line Requests use apiCommandLineVersion. + * The versioning constants dictate the size and index mapping of every + * `MultiApiJson` array in the system — changing them automatically adjusts + * every data structure that stores per-version output. */ +namespace xrpl { + namespace RPC { +/** + * @brief Typed version-constant factory. + * + * Produces an `std::integral_constant` tag for the given + * version number. Using a distinct type per version allows overload resolution + * and `if constexpr` branching at compile time while still implicitly decaying + * to `unsigned` in arithmetic and comparison contexts. + * + * @tparam Version The API version number to encode as a type. + */ template constexpr static std::integral_constant kAPI_VERSION = {}; +/** Sentinel returned by `getAPIVersionNumber()` when parsing fails or the + * supplied version falls outside the supported range. Callers that receive + * this value must reject the request before any handler dispatch. */ constexpr static auto kAPI_INVALID_VERSION = kAPI_VERSION<0>; + +/** Oldest API version still accepted from network clients. Requests with a + * lower version are rejected; the floor advances when old versions are + * retired. */ constexpr static auto kAPI_MINIMUM_SUPPORTED_VERSION = kAPI_VERSION<1>; + +/** Newest stable API version. Network requests are capped here unless the + * `[beta_rpc_api]` configuration flag is set, in which case + * `kAPI_BETA_VERSION` becomes the effective ceiling. */ constexpr static auto kAPI_MAXIMUM_SUPPORTED_VERSION = kAPI_VERSION<2>; + +/** Implicit version assigned when a request omits the `api_version` field. + * Fixed at 1 because any request at version 2 or above must carry an + * explicit field; omitting it is treated as a version-1 request rather than + * an error. This constant will fall below `kAPI_MINIMUM_SUPPORTED_VERSION` + * once version-1 support is retired. */ constexpr static auto kAPI_VERSION_IF_UNSPECIFIED = kAPI_VERSION<1>; -constexpr static auto kAPI_COMMAND_LINE_VERSION = kAPI_VERSION<1>; // TODO Bump to 2 later + +/** Version used for command-line invocations. + * @note TODO: bump to 2 in a future release. */ +constexpr static auto kAPI_COMMAND_LINE_VERSION = kAPI_VERSION<1>; + +/** Experimental version gated behind the `[beta_rpc_api]` configuration flag. + * Completely invisible to clients connecting to a production node that has + * not opted in. */ constexpr static auto kAPI_BETA_VERSION = kAPI_VERSION<3>; + +/** Absolute ceiling for template range loops; always equal to + * `kAPI_BETA_VERSION`. Drives the size of `MultiApiJson` arrays and the + * upper bound of `forAllApiVersions`. */ constexpr static auto kAPI_MAXIMUM_VALID_VERSION = kAPI_BETA_VERSION; +// --- Version-range invariants (load-bearing; update assertions when bumping +// any constant above) --- static_assert(kAPI_INVALID_VERSION < kAPI_MINIMUM_SUPPORTED_VERSION); static_assert( kAPI_VERSION_IF_UNSPECIFIED >= kAPI_MINIMUM_SUPPORTED_VERSION && @@ -56,6 +89,28 @@ static_assert(kAPI_MAXIMUM_SUPPORTED_VERSION >= kAPI_MINIMUM_SUPPORTED_VERSION); static_assert(kAPI_BETA_VERSION >= kAPI_MAXIMUM_SUPPORTED_VERSION); static_assert(kAPI_MAXIMUM_VALID_VERSION >= kAPI_MAXIMUM_SUPPORTED_VERSION); +/** + * @brief Populate the `version` sub-object in an RPC response. + * + * The output format diverges by negotiated version to maintain backwards + * compatibility: + * - **Version 1** (legacy): emits `first`, `good`, and `last` as semver + * strings (e.g. `"1.0.0"`). Static `SemanticVersion` objects are used to + * avoid repeated string parsing on every call. + * - **Version 2+**: emits `first` as the minimum supported version integer + * and `last` as either `kAPI_BETA_VERSION` or `kAPI_MAXIMUM_SUPPORTED_VERSION` + * depending on `betaEnabled`. + * + * The primary consumer is `VersionHandler` in + * `src/xrpld/rpc/handlers/server_info/Version.h`. + * + * @param parent The JSON object into which the `version` key is written. + * @param apiVersion The negotiated API version for the current request; must + * not be `kAPI_INVALID_VERSION`. + * @param betaEnabled Whether the `[beta_rpc_api]` configuration flag is set, + * which extends the reported `last` version to include the + * beta version. + */ inline void setVersion(json::Value& parent, unsigned int apiVersion, bool betaEnabled) { @@ -65,7 +120,7 @@ setVersion(json::Value& parent, unsigned int apiVersion, bool betaEnabled) if (apiVersion == kAPI_VERSION_IF_UNSPECIFIED) { - // API version numbers used in API version 1 + // Legacy semver-string format required by API version 1 clients. static beast::SemanticVersion const kFIRST_VERSION{"1.0.0"}; static beast::SemanticVersion const kGOOD_VERSION{"1.0.0"}; static beast::SemanticVersion const kLAST_VERSION{"1.0.0"}; @@ -82,18 +137,28 @@ setVersion(json::Value& parent, unsigned int apiVersion, bool betaEnabled) } /** - * Retrieve the api version number from the json value + * @brief Extract and validate the API version from an incoming RPC request. * - * Note that APIInvalidVersion will be returned if - * 1) the version number field has a wrong format - * 2) the version number retrieved is out of the supported range - * 3) the version number is unspecified and - * APIVersionIfUnspecified is out of the supported range + * Called at the RPC ingress point (`ServerHandler.cpp`) on every HTTP and + * WebSocket request before handler dispatch. The function inspects the + * top-level `api_version` field of `jv`: + * - If the field is absent, returns `kAPI_VERSION_IF_UNSPECIFIED`. + * - If the field is present but not an integer, returns `kAPI_INVALID_VERSION`. + * - If the integer value falls outside + * `[kAPI_MINIMUM_SUPPORTED_VERSION, maxVersion]`, returns + * `kAPI_INVALID_VERSION`. + * - Otherwise returns the integer value directly. * - * @param jv a Json value that may or may not specify - * the api version number - * @param betaEnabled if the beta API version is enabled - * @return the api version number + * Callers must treat a `kAPI_INVALID_VERSION` return as a signal to reject + * the request immediately with an appropriate error. + * + * @param jv The top-level JSON object of the incoming request. + * @param betaEnabled When `false`, the effective ceiling is + * `kAPI_MAXIMUM_SUPPORTED_VERSION`; when `true`, the ceiling + * extends to `kAPI_BETA_VERSION`. Reflects the + * `BETA_RPC_API` configuration flag of the serving node. + * @return The negotiated API version, or `kAPI_INVALID_VERSION` if the + * request must be rejected. */ inline unsigned int getAPIVersionNumber(json::Value const& jv, bool betaEnabled) @@ -125,6 +190,39 @@ getAPIVersionNumber(json::Value const& jv, bool betaEnabled) } // namespace RPC +/** + * @brief Invoke a callable once for each API version in `[MinVer, MaxVer]`, + * passing the version as a distinct `std::integral_constant` type. + * + * The range is expanded into a parameter pack at compile time via + * `std::make_index_sequence`, and the callable is called once per version in + * order. Because each invocation receives a different type + * (`std::integral_constant`), the callable may use + * `if constexpr (Version >= 2)` to eliminate dead branches at compile time + * rather than relying on a runtime switch. + * + * The C++20 `requires` clause enforces three constraints statically: + * - `MaxVer >= MinVer` (non-empty range), + * - `MinVer >= kAPI_MINIMUM_SUPPORTED_VERSION` (floor bound), + * - `MaxVer <= kAPI_MAXIMUM_VALID_VERSION` (ceiling bound). + * A caller that attempts to iterate outside the known valid range fails to + * compile rather than producing a runtime out-of-bounds error. + * + * @note The `NOLINTBEGIN/NOLINTEND` block suppresses a spurious + * `bugprone-use-after-move` warning that clang-tidy raises on the fold + * expression when `Args` contains move-only types; the fold is safe + * because perfect-forwarding within a comma-expression does not actually + * move from the same argument twice. + * + * @tparam MinVer First version in the iteration range (inclusive). + * @tparam MaxVer Last version in the iteration range (inclusive). + * @tparam Fn Callable type; must be invocable with + * `(std::integral_constant, Args&&...)` for + * every `V` in `[MinVer, MaxVer]`. + * @tparam Args Additional arguments forwarded verbatim to each invocation. + * @param fn The callable to invoke for each version. + * @param args Additional arguments forwarded to each invocation of `fn`. + */ template void forApiVersions(Fn const& fn, Args&&... args) @@ -146,6 +244,28 @@ forApiVersions(Fn const& fn, Args&&... args) }(std::make_index_sequence{}); } +/** + * @brief Invoke a callable once for every supported API version + * (`[kAPI_MINIMUM_SUPPORTED_VERSION, kAPI_MAXIMUM_VALID_VERSION]`). + * + * Thin wrapper around `forApiVersions` that fixes the range to the full set + * of known versions (currently 1–3). This is the standard way to: + * - Run a test scenario against every version in CI. + * - Populate all slots of a `MultiApiJson` fan-out in a single pass (e.g. + * `NetworkOPs.cpp` uses it to build per-subscriber data when notifying of + * new transactions, calling `insertDeliverMax` only for versions where that + * field is defined). + * + * Each invocation of `fn` receives a distinct + * `std::integral_constant` type for the version, enabling + * compile-time branching inside the lambda body. + * + * @tparam Fn Callable type; must satisfy the constraints of + * `forApiVersions` for the full version range. + * @tparam Args Additional arguments forwarded verbatim to each invocation. + * @param fn The callable to invoke for each version. + * @param args Additional arguments forwarded to each invocation of `fn`. + */ template void forAllApiVersions(Fn const& fn, Args&&... args) diff --git a/include/xrpl/protocol/Asset.h b/include/xrpl/protocol/Asset.h index e0f4aa08a2..dc32a0cb9b 100644 --- a/include/xrpl/protocol/Asset.h +++ b/include/xrpl/protocol/Asset.h @@ -1,3 +1,18 @@ +/** + * @file Asset.h + * @brief Unified asset abstraction for XRP, IOU, and MPT value types. + * + * Introduces the `Asset` type, a `std::variant` wrapper that + * represents all three kinds of transferable value on the XRP Ledger: native + * XRP, IOU issued currencies, and Multi-Purpose Tokens (MPT). `Issue` covers + * both XRP and IOU (distinguished by `Issue::native()`), so the variant has + * two arms but three logical asset kinds. + * + * Conversions *to* `Asset` are implicit (from `Issue`, `MPTIssue`, or `MPTID`) + * to preserve backward compatibility with legacy `Issue`-taking APIs. + * Conversions *out* are explicit via `get()` or guarded with `holds()`. + */ + #pragma once #include @@ -11,6 +26,19 @@ namespace xrpl { class STAmount; +/** + * @brief Empty tag type encoding an amount's numeric kind as a template + * parameter. + * + * Carries no data; its sole purpose is to convey compile-time type information + * through a runtime `std::variant`. Code that needs to dispatch on the numeric + * kind of an `Asset` calls `Asset::getAmountType()`, which returns a + * `std::variant, AmountType, + * AmountType>`, and then `std::visit`s over it to select the + * correct templated path. + * + * @tparam T Must be one of `XRPAmount`, `IOUAmount`, or `MPTAmount`. + */ template requires( std::is_same_v || std::is_same_v || @@ -20,13 +48,26 @@ struct AmountType using amount_type = T; }; -/* Used to check for an asset with either badCurrency() - * or MPT with 0 account. +/** + * @brief Sentinel tag used to test whether an `Asset` holds an invalid value. + * + * An `Asset` is "bad" when it holds an `Issue` whose currency equals + * `badCurrency()`, or an `MPTIssue` whose issuer equals `xrpAccount()` (the + * zero-account sentinel). Use `operator==(BadAsset const&, Asset const&)` or + * compare against `badAsset()` rather than inspecting the sub-type directly. + * + * This pattern avoids a separate validity flag or `std::optional`: + * invalid states are represented as well-known sentinel values. */ struct BadAsset { }; +/** + * @brief Returns a reference to the singleton `BadAsset` sentinel. + * + * Prefer `badAsset() == myAsset` over constructing a temporary `BadAsset{}`. + */ inline BadAsset const& badAsset() { @@ -34,16 +75,30 @@ badAsset() return kA; } -/* Asset is an abstraction of three different issue types: XRP, IOU, MPT. - * For historical reasons, two issue types XRP and IOU are wrapped in Issue - * type. Many functions and classes there were first written for Issue - * have been rewritten for Asset. +/** + * @brief Unified representation of an XRP Ledger asset: XRP, IOU, or MPT. + * + * Wraps `std::variant`. Because `Issue` already encodes both + * XRP (via `Issue::native()`) and IOU, the variant has two arms but three + * logical asset kinds. Value semantics and `constexpr` comparisons are + * preserved — no vtables, no heap allocation. + * + * Implicit conversions *from* `Issue`, `MPTIssue`, and `MPTID` allow callers + * to pass those types anywhere an `Asset` is expected. Extraction of the + * concrete sub-type is explicit: guard with `holds()` then call + * `get()`, or use `visit()` for exhaustive dispatch. + * + * `STAmount` stores an `Asset` as its type-identity half and delegates + * `native()`, `integral()`, `holds<>()`, and `get<>()` directly to it. */ class Asset { public: + /** Underlying storage type: one of `Issue` (XRP or IOU) or `MPTIssue`. */ using value_type = std::variant; + /** Currency or MPTID, depending on the active arm. */ using token_type = std::variant; + /** Runtime amount-kind discriminant returned by `getAmountType()`. */ using AmtType = std::variant, AmountType, AmountType>; @@ -51,66 +106,176 @@ private: value_type issue_; public: + /** Constructs a default (XRP) asset. */ Asset() = default; - /** Conversions to Asset are implicit and conversions to specific issue - * type are explicit. This design facilitates the use of Asset. + /** + * @brief Constructs an Asset from an `Issue` (XRP or IOU). + * + * Implicit to preserve backward compatibility with APIs that previously + * accepted `Issue` directly. + * + * @param issue The XRP or IOU issue to wrap. */ Asset(Issue const& issue) : issue_(issue) { } + /** + * @brief Constructs an Asset from an `MPTIssue`. + * + * Implicit so callers can pass an `MPTIssue` wherever `Asset` is expected. + * + * @param mptIssue The MPT issuance to wrap. + */ Asset(MPTIssue const& mptIssue) : issue_(mptIssue) { } + /** + * @brief Constructs an Asset from a raw `MPTID`. + * + * Convenience implicit conversion that wraps the issuance ID in an + * `MPTIssue` before storing it. + * + * @param issuanceID The 192-bit MPT issuance identifier. + */ Asset(MPTID const& issuanceID) : issue_(MPTIssue{issuanceID}) { } + /** + * @brief Returns the issuer of this asset. + * + * For XRP, returns the zero `AccountID` (no real issuer). For IOU, returns + * the issuing account. For MPT, returns the sequence-owner encoded in the + * MPTID. + */ [[nodiscard]] AccountID const& getIssuer() const; + /** + * @brief Returns a const reference to the active sub-type. + * + * @tparam TIss `Issue` or `MPTIssue`. + * @throws std::logic_error if the asset does not hold `TIss`. Guard with + * `holds()` before calling, or use `visit()` for exhaustive + * dispatch. + */ template constexpr TIss const& get() const; + /** + * @brief Returns a mutable reference to the active sub-type. + * + * @tparam TIss `Issue` or `MPTIssue`. + * @throws std::logic_error if the asset does not hold `TIss`. + */ template TIss& get(); + /** + * @brief Tests whether the asset currently holds the given sub-type. + * + * @tparam TIss `Issue` or `MPTIssue`. + * @return `true` if the active arm matches `TIss`. + */ template [[nodiscard]] constexpr bool holds() const; + /** + * @brief Returns a human-readable string identifying the asset. + * + * Delegates to the underlying `Issue` or `MPTIssue` text representation. + */ [[nodiscard]] std::string getText() const; + /** + * @brief Returns a const reference to the underlying `variant` storage. + * + * Prefer `visit()` or `get()` for type-safe access; this accessor + * is available for callers that must interact with the variant directly. + */ [[nodiscard]] constexpr value_type const& value() const; + /** + * @brief Returns the currency token identity of this asset. + * + * For XRP and IOU assets, returns the `Currency`. For MPT assets, returns + * the `MPTID`. Useful when identity must be compared independently of the + * issuer. + */ [[nodiscard]] constexpr token_type token() const; + /** + * @brief Serializes the asset into a JSON value. + * + * For IOU: emits `currency` and `issuer` keys (no issuer for XRP). + * For MPT: emits `mpt_issuance_id`. + * + * @param jv Output JSON object; populated in place. + */ void setJson(json::Value& jv) const; + /** + * @brief Constructs an `STAmount` from this asset and a raw numeric value. + * + * Convenience operator enabling concise amount construction: + * `myAsset(someNumber)`. The `Number` is interpreted according to the + * asset's kind (XRP drops, IOU mantissa/exponent, MPT integer). + * + * @param n The numeric value to associate with this asset. + * @return An `STAmount` holding this asset and the given value. + */ STAmount operator()(Number const&) const; + /** + * @brief Returns a tag-variant encoding the runtime amount kind. + * + * The returned variant holds one of `AmountType`, + * `AmountType`, or `AmountType`. `std::visit` over + * this result to select the correct templated arithmetic path without + * inspecting the asset sub-type manually. + */ [[nodiscard]] constexpr AmtType getAmountType() const; - // Custom, generic visit implementation + /** + * @brief Applies a set of lambdas to the active `Issue` or `MPTIssue` arm. + * + * Combines the provided callables into a single overload set using + * `detail::visit` (the `CombineVisitors` trick from `Concepts.h`) and + * forwards to `std::visit` over the internal variant. Example: + * @code + * asset.visit( + * [](Issue const& issue) { / * XRP or IOU * / }, + * [](MPTIssue const& mpt) { / * MPT * / }); + * @endcode + * + * @tparam Visitors Callable types whose signatures cover `Issue` and `MPTIssue`. + * @return The return value of the matching visitor. + */ template constexpr auto visit(Visitors&&... visitors) const -> decltype(auto) { - // Simple delegation to the reusable utility, passing the internal - // variant data. return detail::visit(issue_, std::forward(visitors)...); } + /** + * @brief Returns `true` if and only if the asset is native XRP. + * + * MPT always returns `false`; IOU always returns `false`; only the XRP + * arm of `Issue` returns `true`. + */ [[nodiscard]] constexpr bool native() const { @@ -119,6 +284,14 @@ public: [&](MPTIssue const&) { return false; }); } + /** + * @brief Returns `true` if the asset has an integer (non-fractional) amount + * representation. + * + * Both XRP (drops) and MPT amounts are always whole numbers. IOU amounts + * use a floating-point mantissa/exponent encoding and are not integral. + * This distinction affects serialization and arithmetic rounding. + */ [[nodiscard]] bool integral() const { @@ -127,32 +300,83 @@ public: [&](MPTIssue const&) { return true; }); } + /** + * @brief Equality: `true` when both assets hold the same sub-type and + * compare equal within that sub-type. + * + * Cross-type comparisons (e.g., `Issue` vs `MPTIssue`) always return + * `false`. For IOU, both currency and issuer must match. Use + * `equalTokens()` to compare ignoring issuer. + */ friend constexpr bool operator==(Asset const& lhs, Asset const& rhs); + /** + * @brief Total order over assets for use in sorted containers. + * + * When both assets hold the same variant arm, ordering is delegated to + * that arm's natural `<=>`. When arms differ, `Issue` sorts greater than + * `MPTIssue` (an arbitrary but stable convention). + */ friend constexpr std::weak_ordering operator<=>(Asset const& lhs, Asset const& rhs); + /** + * @brief Tests whether the asset holds an `Issue` with the given currency. + * + * Returns `false` for any `MPTIssue` asset regardless of `lhs`. + * + * @param lhs The currency to compare against. + * @param rhs The asset to inspect. + */ friend constexpr bool operator==(Currency const& lhs, Asset const& rhs); - // rhs is either badCurrency() or MPT issuer is 0 + /** + * @brief Tests whether the asset represents an invalid (sentinel) value. + * + * Returns `true` when `rhs` holds an `Issue` with `badCurrency()`, or an + * `MPTIssue` whose issuer is `xrpAccount()` (the zero-account sentinel). + * + * @param lhs Unused sentinel tag; use `badAsset()` as the left operand. + * @param rhs The asset to test. + */ friend constexpr bool operator==(BadAsset const& lhs, Asset const& rhs); - /** Return true if both assets refer to the same currency (regardless of - * issuer) or MPT issuance. Otherwise return false. + /** + * @brief Returns `true` if both assets refer to the same token type, + * regardless of issuer. + * + * For `Issue`-vs-`Issue` comparisons only the `Currency` field is checked; + * issuers are ignored. For `MPTIssue`-vs-`MPTIssue` the full `MPTID` is + * compared (issuer is already encoded in the ID, so there is no + * issuer-free concept). Cross-type comparisons always return `false`. + * + * Used in path-finding and offer-matching where token type must match but + * trust lines from different issuers in the same currency are acceptable. */ friend constexpr bool equalTokens(Asset const& lhs, Asset const& rhs); }; +/** @brief `true` when `TIss` is `Issue`. Helper for `operator<=>`. */ template constexpr bool kIS_ISSUE_V = std::is_same_v; +/** @brief `true` when `TIss` is `MPTIssue`. Helper for `operator<=>`. */ template constexpr bool kIS_MPTISSUE_V = std::is_same_v; +/** + * @brief Converts an asset to a `json::Value` representation. + * + * For IOU: produces `{currency, issuer}` (no issuer key for XRP). + * For MPT: produces `{mpt_issuance_id}`. + * + * @param asset The asset to serialize. + * @return A `json::Value` object describing the asset. + */ inline json::Value toJson(Asset const& asset) { @@ -293,21 +517,65 @@ equalTokens(Asset const& lhs, Asset const& rhs) rhs.issue_); } +/** + * @brief Returns `true` if the asset is native XRP. + * + * Thin wrapper around `Asset::native()` for readability at call sites. + * + * @param asset The asset to test. + */ inline bool isXRP(Asset const& asset) { return asset.native(); } +/** + * @brief Returns a human-readable string representation of an asset. + * + * Delegates to the underlying `Issue` or `MPTIssue` text form. Suitable for + * logging and error messages; not for wire serialization. + * + * @param asset The asset to stringify. + * @return A descriptive string identifying the asset. + */ std::string to_string(Asset const& asset); +/** + * @brief Validates that a JSON object encodes a well-formed asset. + * + * Enforces the protocol rule that an asset JSON object must contain exactly + * one of `currency` or `mpt_issuance_id`, but not both. + * + * @param jv The JSON value to validate. + * @return `true` if the JSON represents a valid asset; `false` otherwise. + */ bool validJSONAsset(json::Value const& jv); +/** + * @brief Parses an `Asset` from a JSON value. + * + * Accepts either `{currency[, issuer]}` for XRP/IOU or + * `{mpt_issuance_id}` for MPT. Throws on malformed input. + * + * @param jv The JSON object describing the asset. + * @return The parsed `Asset`. + * @throws std::runtime_error (or equivalent) if `jv` is not a valid asset. + */ Asset assetFromJson(json::Value const& jv); +/** + * @brief Returns `true` if the asset's internal fields are mutually consistent. + * + * For XRP/IOU assets, delegates to `Issue::isConsistent()` which checks that + * XRP has no account component. MPT assets are always considered consistent. + * Less strict than `validAsset()` — does not reject sentinel currencies. + * + * @param asset The asset to check. + */ inline bool isConsistent(Asset const& asset) { @@ -316,6 +584,15 @@ isConsistent(Asset const& asset) [](MPTIssue const&) { return true; }); } +/** + * @brief Returns `true` if the asset is a well-formed, non-sentinel value. + * + * Stricter than `isConsistent()`: additionally rejects `badCurrency()` for + * IOU/XRP assets and the zero-issuer sentinel for MPT assets. Use this to + * validate user-provided or deserialized assets before operating on them. + * + * @param asset The asset to validate. + */ inline bool validAsset(Asset const& asset) { @@ -324,6 +601,17 @@ validAsset(Asset const& asset) [](MPTIssue const& issue) { return issue.getIssuer() != xrpAccount(); }); } +/** + * @brief Appends an asset's hash contribution to a Hasher. + * + * Enables `Asset` as a key in `beast::uhash`-based and `std::unordered_*` + * containers. Dispatches to the active arm's own `hash_append` specialization, + * so `Issue` and `MPTIssue` assets produce distinct hash domains. + * + * @tparam Hasher A `beast::hash_append`-compatible hasher type. + * @param h The hasher to accumulate into. + * @param r The asset to hash. + */ template void hash_append(Hasher& h, Asset const& r) @@ -334,6 +622,15 @@ hash_append(Hasher& h, Asset const& r) [&](MPTIssue const& issue) { hash_append(h, issue); }); } +/** + * @brief Stream-inserts a human-readable asset description. + * + * Equivalent to `os << to_string(x)`. Intended for logging and diagnostics. + * + * @param os The output stream. + * @param x The asset to write. + * @return `os`, for chaining. + */ std::ostream& operator<<(std::ostream& os, Asset const& x); diff --git a/include/xrpl/protocol/Batch.h b/include/xrpl/protocol/Batch.h index fa7641af70..b10bd4d889 100644 --- a/include/xrpl/protocol/Batch.h +++ b/include/xrpl/protocol/Batch.h @@ -1,9 +1,50 @@ +/** @file + * Defines the canonical wire-format serialization for batch signing payloads. + * + * A batch payload is the exact byte sequence that every co-signer of a + * `ttBATCH` transaction signs and that validators verify. The format is + * protocol-stable: any reordering of the four serialized fields would + * invalidate all previously issued batch signatures. + */ + #include #include #include namespace xrpl { +/** Serialize the signable payload for a batch transaction. + * + * Appends four fields to `msg` in a fixed order: + * 1. `HashPrefix::batch` — 4-byte domain separator that places batch hashes + * in their own hash-space, preventing cross-type signature collisions. + * 2. `flags` — the outer batch transaction's execution-policy flags (e.g. + * `tfAllOrNothing`). Signing over the flags ensures a signer cannot have + * the execution policy changed after they have committed. + * 3. `txids.size()` — the inner-transaction count as a `uint32_t`. Explicit + * serialization of the count prevents an adversary from extending or + * truncating the list without invalidating signatures. + * 4. Each `uint256` in `txids` — the hash of each inner transaction, in + * order. Signers commit to the exact set of inner transactions by ID. + * + * Both `checkBatchSingleSign()` and `checkBatchMultiSign()` in `STTx.cpp` + * call this function to build the verification payload, and test signing + * helpers do the same, so signing and verification share a single + * serialization path. For multi-sign, `serializeBatch()` is called once + * and `finishMultiSigningData()` appends each per-signer account ID suffix + * without re-serializing the inner transaction list. + * + * @param msg Serializer that receives the batch payload bytes. The caller + * is responsible for passing the resulting `msg.slice()` to the + * appropriate signature primitive. + * @param flags The `uint32_t` flags field of the outer batch transaction, + * as returned by `STTx::getFlags()`. + * @param txids Ordered list of inner-transaction IDs, as returned by + * `STTx::getBatchTransactionIDs()`. + * + * @note `HashPrefix::batch` is a protocol constant. Changing it would + * invalidate all existing batch signatures and requires an amendment. + */ inline void serializeBatch(Serializer& msg, std::uint32_t const& flags, std::vector const& txids) { diff --git a/include/xrpl/protocol/Book.h b/include/xrpl/protocol/Book.h index fc36abddc4..873c31a766 100644 --- a/include/xrpl/protocol/Book.h +++ b/include/xrpl/protocol/Book.h @@ -1,3 +1,9 @@ +/** @file + * Defines the `Book` type — the identity of an XRPL DEX order book — together + * with `std::hash` and `boost::hash` specializations for `Issue`, `MPTIssue`, + * `Asset`, and `Book` needed by unordered containers throughout the codebase. + */ + #pragma once #include @@ -8,34 +14,110 @@ namespace xrpl { -/** Specifies an order book. - The order book is a pair of Issues called in and out. - @see Issue. -*/ +/** Identity of an XRPL order book: a directed pair of assets. + * + * An order book is the set of all open offers to exchange one asset for + * another in a specific direction. `in` is the asset a taker spends; + * `out` is the asset a taker receives. Because `Asset` is a variant of + * `Issue` and `MPTIssue`, a `Book` can represent any combination of XRP, + * IOU, and MPT asset classes. + * + * When `domain` is set, the book is scoped to a `PermissionedDomain` + * ledger entry identified by that `uint256` index. A domain-scoped book + * and the corresponding open book are distinct even when their `in`/`out` + * assets are identical — equality, ordering, and hashing all include + * `domain`. + * + * Inherits `CountedObject` for diagnostic instance counting only; + * this has no effect on protocol logic. + * + * @invariant A well-formed book satisfies `isConsistent(*this)`: + * both legs are individually consistent and `in != out`. + * @see isConsistent, reversed + */ class Book final : public CountedObject { public: + /** The asset being spent by the taker (offered). */ Asset in; + + /** The asset being received by the taker (wanted). */ Asset out; + + /** Optional permissioned-domain scope for this order book. + * + * When present, the `uint256` is the ledger index of a + * `PermissionedDomain` object that gates participation. Absent means + * the book is open to all accounts. + */ std::optional domain; Book() = default; + /** Construct a Book from explicit asset legs and an optional domain. + * + * @param in Asset being spent by the taker. + * @param out Asset being received by the taker. + * @param domain Ledger index of the `PermissionedDomain` that scopes + * this book, or `std::nullopt` for an open book. + */ Book(Asset const& in, Asset const& out, std::optional const& domain) : in(in), out(out), domain(domain) { } }; +/** Check that a Book is self-consistent. + * + * A book is consistent when both `in` and `out` are individually consistent + * (e.g., no XRP currency paired with a non-XRP issuer) and `in != out`. + * A book with identical legs would represent trading an asset against itself + * and would cause infinite-loop offer matching. + * + * @note `book.domain` is not validated here; semantic validity of the domain + * identifier belongs to higher-level transaction processing. + * @param book The order book to validate. + * @return `true` if both legs are individually consistent and `in != out`. + */ bool isConsistent(Book const& book); +/** Format a Book as a human-readable string for logging and diagnostics. + * + * Produces a string of the form `"->"` using the `to_string` + * representations of each asset leg. The arrow makes directionality + * explicit. This format is not part of the wire protocol. + * + * @param book The order book to convert. + * @return A string of the form `"->"`. + */ std::string to_string(Book const& book); +/** Write a Book to an output stream in diagnostic form. + * + * Delegates to `to_string(book)`. + * + * @param os The output stream to write to. + * @param x The order book to write. + * @return `os`, to allow chaining. + */ std::ostream& operator<<(std::ostream& os, Book const& x); +/** Append a Book to a cryptographic hash state (beast hash pipeline). + * + * Hashes `in` and `out` unconditionally, then appends `domain` only when + * present. A book with a domain and one without — even with identical + * asset legs — therefore produce different digests. This matters for + * ledger index derivation in `Indexes.cpp`, where the presence of a domain + * conditionally changes the hash inputs used to locate the book's offer + * directory. + * + * @tparam Hasher A beast-compatible hash accumulator. + * @param h The hash state to append to. + * @param b The order book whose fields are appended. + */ template void hash_append(Hasher& h, Book const& b) @@ -46,11 +128,26 @@ hash_append(Hasher& h, Book const& b) hash_append(h, *(b.domain)); } +/** Return the mirror-image order book with `in` and `out` swapped. + * + * Preserves `domain` unchanged — a domain-scoped market is the same market + * when viewed from either direction. Used by the Subscribe/Unsubscribe RPC + * handlers when a client requests the `both` flag so that updates from both + * the bid and ask sides are delivered. + * + * @param book The order book to reverse. + * @return A new `Book` with `in` and `out` exchanged and `domain` unchanged. + */ Book reversed(Book const& book); -/** Equality comparison. */ /** @{ */ +/** Test two Books for equality. + * + * Two books are equal only when `in`, `out`, and `domain` all compare equal. + * A domain-scoped book is never equal to an open book with the same asset + * legs. + */ [[nodiscard]] constexpr bool operator==(Book const& lhs, Book const& rhs) { @@ -58,8 +155,19 @@ operator==(Book const& lhs, Book const& rhs) } /** @} */ -/** Strict weak ordering. */ /** @{ */ +/** Three-way comparison establishing a strict weak ordering over Books. + * + * Orders first by `in`, then by `out`, then by `domain`. An absent domain + * compares less than any present domain. This ordering is used by sorted + * containers such as subscription routing tables and by `BookDirs` traversal + * to iterate over offer directories deterministically. + * + * @note The optional comparison is performed manually (rather than relying + * on the standard library's spaceship support for `optional`) to + * guarantee a `std::weak_ordering` return type consistent with the + * `Asset` spaceship result. + */ [[nodiscard]] constexpr std::weak_ordering operator<=>(Book const& lhs, Book const& rhs) { @@ -68,15 +176,15 @@ operator<=>(Book const& lhs, Book const& rhs) if (auto const c{lhs.out <=> rhs.out}; c != 0) return c; - // Manually compare optionals + // Manually compare optionals: absent domain sorts before any present domain. if (lhs.domain && rhs.domain) - return *lhs.domain <=> *rhs.domain; // Compare values if both exist + return *lhs.domain <=> *rhs.domain; if (!lhs.domain && rhs.domain) - return std::weak_ordering::less; // Empty is considered less + return std::weak_ordering::less; if (lhs.domain && !rhs.domain) - return std::weak_ordering::greater; // Non-empty is greater + return std::weak_ordering::greater; - return std::weak_ordering::equivalent; // Both are empty + return std::weak_ordering::equivalent; } /** @} */ @@ -86,6 +194,13 @@ operator<=>(Book const& lhs, Book const& rhs) namespace std { +/** `std::hash` specialization for `xrpl::Issue`. + * + * Combines the currency hash with the account (issuer) hash via + * `boost::hash_combine`. For XRP, the issuer field is ignored because + * all XRP issuers are equivalent by protocol definition, ensuring that + * all representations of XRP hash identically. + */ template <> struct hash : private boost::base_from_member, 0>, private boost::base_from_member, 1> @@ -110,6 +225,12 @@ public: } }; +/** `std::hash` specialization for `xrpl::MPTIssue`. + * + * Hashes only the 192-bit `MPTID` (32-bit sequence number concatenated with + * the 160-bit issuer account), which is the canonical unique identifier for + * an MPT issuance. + */ template <> struct hash : private boost::base_from_member, 0> { @@ -130,6 +251,11 @@ public: } }; +/** `std::hash` specialization for `xrpl::Asset`. + * + * Visits the underlying variant and dispatches to the appropriate + * `std::hash` or `std::hash` specialization. + */ template <> struct hash { @@ -163,6 +289,14 @@ public: //------------------------------------------------------------------------------ +/** `std::hash` specialization for `xrpl::Book`. + * + * Seeds from `hash(in)`, combines `hash(out)` via `boost::hash_combine`, + * then conditionally combines `hash(domain)` when a domain is present. + * A domain-scoped book and an otherwise-identical open book therefore + * produce distinct hash values, which is required for correct keying in + * subscription routing tables and offer-directory indexes. + */ template <> struct hash { @@ -198,6 +332,16 @@ public: namespace boost { +/** `boost::hash` adapter for `xrpl::Issue`. + * + * Inherits `std::hash` so that Boost.Unordered and + * Boost.MultiIndex containers resolve the same hash function as standard + * unordered containers, avoiding divergence between the two hash registries. + * + * @note Constructor inheritance (`using Base::Base`) is omitted because it + * was broken in Visual Studio 2012; an explicit defaulted constructor + * is provided instead. + */ template <> struct hash : std::hash { @@ -208,6 +352,11 @@ struct hash : std::hash // using Base::Base; // inherit ctors }; +/** `boost::hash` adapter for `xrpl::MPTIssue`. + * + * Delegates to `std::hash` so that Boost containers use + * the same hash logic as standard containers. + */ template <> struct hash : std::hash { @@ -216,6 +365,11 @@ struct hash : std::hash using Base = std::hash; }; +/** `boost::hash` adapter for `xrpl::Asset`. + * + * Delegates to `std::hash` so that Boost containers use the + * same variant-dispatching hash logic as standard containers. + */ template <> struct hash : std::hash { @@ -224,6 +378,15 @@ struct hash : std::hash using Base = std::hash; }; +/** `boost::hash` adapter for `xrpl::Book`. + * + * Delegates to `std::hash` so that Boost containers use the + * same domain-aware hash logic as standard containers. + * + * @note Constructor inheritance (`using Base::Base`) is omitted because it + * was broken in Visual Studio 2012; an explicit defaulted constructor + * is provided instead. + */ template <> struct hash : std::hash { diff --git a/include/xrpl/protocol/BuildInfo.h b/include/xrpl/protocol/BuildInfo.h index 47a27339a8..744baa27cf 100644 --- a/include/xrpl/protocol/BuildInfo.h +++ b/include/xrpl/protocol/BuildInfo.h @@ -1,76 +1,135 @@ +/** @file + * Version identity and wire-encoding utilities for the xrpld binary. + * + * Owns the canonical SemVer version string, the composite `systemName-version` + * identifier used in HTTP headers and peer-protocol handshakes, and a compact + * 64-bit encoding that lets validators compare software versions during + * consensus via a plain integer comparison — no string parsing required at + * runtime. + * + * At every flag ledger (every 256 ledgers) `RCLConsensus` writes the result of + * `getEncodedVersion()` into `sfServerVersion` in each validation message it + * broadcasts. `LedgerMaster` then inspects incoming validations, calling + * `isXrpldVersion()` and `isNewerVersion()` to count how many UNL validators + * are running a newer build, which can surface an upgrade notification. + */ + #pragma once #include #include -/** Versioning information for this build. */ +/** Version identity, wire encoding, and peer-comparison utilities for xrpld. + * + * @deprecated The `BuildInfo` sub-namespace is expected to be dissolved; these + * utilities will eventually be promoted directly into `xrpl`. + */ // VFALCO The namespace is deprecated namespace xrpl::BuildInfo { -/** Server version. - Follows the Semantic Versioning Specification: - http://semver.org/ -*/ +/** Return the canonical SemVer version string for this build. + * + * The result is memoized; the initializer runs exactly once (C++11 + * thread-safe static-init guarantee). On first call the hard-coded + * `versionString` constant is round-tripped through `beast::SemanticVersion`: + * if it fails to parse, or if its canonical re-serialization differs from the + * original, `LogicError` is thrown and the process terminates. This acts as a + * start-up invariant check — a malformed version constant is caught + * immediately rather than producing silently wrong encoded integers. + * + * In `DEBUG` or sanitizer builds, SemVer build metadata (commit hash, + * `DEBUG`, sanitizer names) is appended as a `+`-separated suffix, e.g. + * `"3.2.0-b0+abc1234.DEBUG"`. + * + * @return a reference to the cached, validated version string (e.g. + * `"3.2.0-b0"`). + * @throw LogicError on first call if `versionString` is malformed or + * not in canonical SemVer form. + */ std::string const& getVersionString(); -/** Full server version string. - This includes the name of the server. It is used in the peer - protocol hello message and also the headers of some HTTP replies. -*/ +/** Return the composite `systemName-version` string for this build. + * + * Prepends `systemName()` (always `"xrpld"`) to `getVersionString()`, + * separated by `"-"`, e.g. `"xrpld-3.2.0-b0"`. This string appears verbatim + * in the `User-Agent` and `Server` HTTP headers during peer-protocol + * handshakes and in all HTTP responses from the JSON-RPC server. + * + * @return a reference to the cached composite version string. + */ std::string const& getFullVersionString(); -/** Encode an arbitrary server software version in a 64-bit integer. - - The general format is: - - ........-........-........-........-........-........-........-........ - XXXXXXXX-XXXXXXXX-YYYYYYYY-YYYYYYYY-YYYYYYYY-YYYYYYYY-YYYYYYYY-YYYYYYYY - - X: 16 bits identifying the particular implementation - Y: 48 bits of data specific to the implementation - - The xrpld-specific format (implementation ID is: 0x18 0x3B) is: - - 00011000-00111011-MMMMMMMM-mmmmmmmm-pppppppp-TTNNNNNN-00000000-00000000 - - M: 8-bit major version (0-255) - m: 8-bit minor version (0-255) - p: 8-bit patch version (0-255) - T: 11 if neither an RC nor a beta - 10 if an RC - 01 if a beta - N: 6-bit rc/beta number (1-63) - - @param the version string - @return the encoded version in a 64-bit integer -*/ +/** Encode a SemVer string into the 64-bit wire format used in `sfServerVersion`. + * + * The resulting integer has the following bit layout: + * + * ``` + * [63:48] implementation identifier (0x183B for xrpld) + * [47:40] major version (8 bits, 0-255) + * [39:32] minor version (8 bits, 0-255) + * [31:24] patch version (8 bits, 0-255) + * [23:22] pre-release type (0b11 = release, 0b10 = RC, 0b01 = beta) + * [21:16] pre-release number (6 bits, 0-63; 0 for releases) + * [15:0] reserved zeros + * ``` + * + * The pre-release type bits are deliberately ordered so that a plain integer + * comparison on the full `uint64_t` yields correct semantic ordering: + * release (`0b11`) > RC (`0b10`) > beta (`0b01`). A malformed pre-release + * identifier (missing number, non-numeric suffix, number out of range + * [0, 63]) silently yields zero for bits [23:16], which sorts below any + * recognizable pre-release type. If `versionStr` does not parse as valid + * SemVer at all, the return value contains only the xrpld fingerprint + * (`0x183B`) in bits [63:48] and zeros elsewhere. + * + * @param versionStr a SemVer-formatted version string (e.g. `"3.2.0-b0"`). + * @return the packed version as a `uint64_t`; see bit layout above. + */ std::uint64_t encodeSoftwareVersion(std::string_view versionStr); -/** Returns this server's version packed in a 64-bit integer. */ +/** Return this node's own encoded version, cached from `getVersionString()`. + * + * Calls `encodeSoftwareVersion(getVersionString())` exactly once and caches + * the result as a function-local static. This value is written into + * `sfServerVersion` in every validation message emitted on flag ledgers by + * `RCLConsensus`. + * + * @return the cached 64-bit encoded version for this build. + */ std::uint64_t getEncodedVersion(); -/** Check if the encoded software version is an xrpld software version. - - @param version another node's encoded software version - @return true if the version is an xrpld software version, false otherwise -*/ +/** Return true if `version` carries the xrpld implementation fingerprint. + * + * Checks only the upper 16 bits against the xrpld identifier `0x183B`. This + * must be called before any numeric comparison of version values: a non-xrpld + * peer could advertise an arbitrarily large integer that would otherwise + * appear "newer", so `isNewerVersion()` calls this guard unconditionally. + * + * @param version an encoded software version read from `sfServerVersion`. + * @return true iff the upper 16 bits of `version` equal `0x183B`. + */ bool isXrpldVersion(std::uint64_t version); -/** Check if the version is newer than the local node's xrpld software - version. - - @param version another node's encoded software version - @return true if the version is newer than the local node's xrpld software - version, false otherwise. - - @note This function only understands version numbers that are generated by - xrpld. Please see the encodeSoftwareVersion() function for detail. -*/ +/** Return true if `version` represents a strictly newer xrpld release than + * this node. + * + * Guards against non-xrpld peers by calling `isXrpldVersion()` first: any + * value whose upper 16 bits differ from `0x183B` unconditionally returns + * false, regardless of its numeric magnitude. For confirmed xrpld versions, + * a plain integer comparison is sufficient because `encodeSoftwareVersion()` + * places major, minor, patch, and release-type bits in descending order of + * significance. + * + * @param version an encoded software version read from `sfServerVersion`. + * @return true iff `version` is an xrpld version strictly greater than + * `getEncodedVersion()`. + * @see isXrpldVersion(), encodeSoftwareVersion() + */ bool isNewerVersion(std::uint64_t version); diff --git a/include/xrpl/protocol/Concepts.h b/include/xrpl/protocol/Concepts.h index e909aff805..6601de3f9b 100644 --- a/include/xrpl/protocol/Concepts.h +++ b/include/xrpl/protocol/Concepts.h @@ -1,3 +1,12 @@ +/** @file + * Compile-time type vocabulary for the XRPL protocol layer. + * + * Centralises all C++20 concept definitions that constrain the three payment + * asset families (XRP, IOU, and MPT) and provides the `detail::CombineVisitors` + * utility used by `Asset::visit()` and `PathAsset::visit()`. Keeping every + * constraint in one place means that adding a new asset family requires + * updates in a single file; the compiler propagates errors to every call site. + */ #pragma once #include @@ -14,20 +23,73 @@ class IOUAmount; class XRPAmount; class MPTAmount; +/** Constrains the three numeric types used as individual payment-step quantities. + * + * `EitherAmount` (the type-erased amount carrier used by the path-finding + * engine) restricts its constructor, `holds()`, and `get()` with this + * concept, so the compiler rejects any attempt to store or query an amount + * type outside the sanctioned set at instantiation time. + * + * @tparam A One of `XRPAmount`, `IOUAmount`, or `MPTAmount`. + */ template concept StepAmount = std::is_same_v || std::is_same_v || std::is_same_v; +/** Constrains template parameters to the two issue types held by `Asset`. + * + * Gates `Asset::get()` and `Asset::holds()` to exactly `Issue` and + * `MPTIssue` — the two alternatives of `Asset`'s internal + * `std::variant`. Also used by the `kIS_ISSUE_V` and + * `kIS_MPTISSUE_V` boolean constants in `Asset.h` that drive `if constexpr` + * branches in comparison operators. + * + * @tparam TIss `Issue` or `MPTIssue`. + */ template concept ValidIssueType = std::is_same_v || std::is_same_v; +/** Constrains template parameters to any type convertible to a known asset representation. + * + * Broader than `ValidIssueType`: uses `is_convertible_v` rather than + * `is_same_v`, so it accepts any type with an implicit conversion path to + * `Asset`, `Issue`, `MPTIssue`, or `MPTID`. This enables generic code that + * accepts any "asset-like" value without requiring callers to normalise to a + * canonical form first. + * + * @tparam A Any type implicitly convertible to `Asset`, `Issue`, `MPTIssue`, or `MPTID`. + */ template concept AssetType = std::is_convertible_v || std::is_convertible_v || std::is_convertible_v || std::is_convertible_v; +/** Constrains template parameters to the two token-identity types used in payment paths. + * + * `PathAsset` carries only the currency/token specifier inside a payment path + * element — it explicitly does not carry issuer information. `Currency` covers + * both XRP (the zero currency) and IOU tokens; `MPTID` covers MPT issuances. + * This concept gates `PathAsset::get()`, `PathAsset::holds()`, and the + * `kIS_CURRENCY_V`/`kIS_MPTID_V` helper constants in `PathAsset.h`. + * + * @tparam T `Currency` or `MPTID`. + */ template concept ValidPathAsset = (std::is_same_v || std::is_same_v); +/** Constrains a pair of step-amount types to a legal DEX trading pair. + * + * Both sides must independently be one of `XRPAmount`, `IOUAmount`, or + * `MPTAmount`, but the XRP/XRP combination is structurally illegal on the + * XRPL order book — an offer cannot have both `TakerPays` and `TakerGets` + * denominated in XRP. `OfferStream::shouldRmSmallIncreasedQOffer()` uses this + * concept to encode that invariant at the type system level rather than as a + * runtime assertion. + * + * @tparam TTakerPays The amount type for what the taker pays; must satisfy `StepAmount`. + * @tparam TTakerGets The amount type for what the taker receives; must satisfy `StepAmount`. + * @note The constraint is equivalent to: both sides are valid step-amount types + * AND NOT (TTakerPays == XRPAmount AND TTakerGets == XRPAmount). + */ template concept ValidTaker = ((std::is_same_v || std::is_same_v || @@ -38,46 +100,70 @@ concept ValidTaker = namespace detail { -// This template combines multiple callable objects (lambdas) into a single -// object that std::visit can use for overload resolution. +/** Combines multiple callable objects (lambdas) into a single overload set for `std::visit`. + * + * Implements the classical *overloaded* pattern: by inheriting from every + * lambda type and pulling each `operator()` into the derived scope, this + * struct becomes a single callable that overload-resolution can dispatch + * correctly based on the active variant alternative at runtime. + * + * Prefer constructing instances via `makeCombineVisitors()` rather than + * direct construction; the factory applies `std::decay_t` and uses function + * template argument deduction, which is more portable than CTAD for variadic + * class templates. + * + * @tparam Ts Lambda (or other callable) types to merge into one overload set. + * @see makeCombineVisitors + */ template struct CombineVisitors : Ts... { - // Bring all operator() overloads from base classes into this scope. - // It's the mechanism that makes the CombineVisitors struct function - // as a single callable object with multiple overloads. using Ts::operator()...; - // Perfect forwarding constructor to correctly initialize the base class - // lambdas + /** Initialises each base-class lambda by perfect-forwarding its argument. */ constexpr CombineVisitors(Ts&&... ts) : Ts(std::forward(ts))... { } }; -// This function forces function template argument deduction, which is more -// robust than class template argument deduction (CTAD) via the deduction guide. +/** Creates a `CombineVisitors` from a pack of callables. + * + * Preferred over a CTAD deduction guide because function template argument + * deduction handles parameter packs more robustly than class-template + * argument deduction (CTAD) across compilers. `std::decay_t` strips + * references and cv-qualifiers from lambda types before they become base + * classes, ensuring the inherited `operator()` calls have the correct value + * categories. + * + * @tparam Ts Callable types; typically lambdas. + * @param ts Callables to combine. + * @return A `CombineVisitors...>` holding all overloads. + */ template constexpr CombineVisitors...> makeCombineVisitors(Ts&&... ts) { - // std::decay_t is used to remove references/constness from the lambda - // types before they are passed as template arguments to the CombineVisitors - // struct. return CombineVisitors...>{std::forward(ts)...}; } -// This function takes ANY variant and ANY number of visitors, and performs the -// visit. It is the reusable core logic. +/** Visits a variant with a set of per-alternative callables. + * + * Combines `visitors...` into a single overload set via `makeCombineVisitors` + * and delegates to `std::visit`. This is the reusable core called by + * `Asset::visit()` and `PathAsset::visit()`; callers should go through those + * member functions rather than invoking this directly. + * + * @tparam Variant A `std::variant` specialisation. + * @tparam Visitors Callable types, one per variant alternative. + * @param v The variant to dispatch on. + * @param visitors Callables covering each alternative of `v`. + * @return The return value of the selected visitor. + */ template constexpr auto visit(Variant&& v, Visitors&&... visitors) -> decltype(auto) { - // Use the function template helper instead of raw CTAD. auto visitorSet = makeCombineVisitors(std::forward(visitors)...); - - // Delegate to std::visit, perfectly forwarding the variant and the visitor - // set. return std::visit(visitorSet, std::forward(v)); } diff --git a/include/xrpl/protocol/ErrorCodes.h b/include/xrpl/protocol/ErrorCodes.h index a91adfc55a..738f693f64 100644 --- a/include/xrpl/protocol/ErrorCodes.h +++ b/include/xrpl/protocol/ErrorCodes.h @@ -1,5 +1,16 @@ #pragma once +/** @file + * Single source of truth for every RPC error the XRPL node can emit. + * + * Defines the stable numeric code space (`ErrorCodeI`), a parallel warning + * code space (`WarningCodeI`), the `ErrorInfo` struct that binds each code + * to a token and HTTP status, and the complete vocabulary of JSON helpers + * used by RPC handlers to produce well-formed error responses. Every + * component that rejects an RPC call — from malformed-parameter checks to + * ledger-not-found conditions — funnels through this file. + */ + #include #include @@ -7,158 +18,174 @@ namespace xrpl { // VFALCO NOTE These are outside the RPC namespace -// NOTE: Although the precise numeric values of these codes were never -// intended to be stable, several API endpoints include the numeric values. -// Some users came to rely on the values, meaning that renumbering would be -// a breaking change for those users. -// -// We therefore treat the range of values as stable although they are not -// and are subject to change. -// -// Please only append to this table. Do not "fill-in" gaps and do not re-use -// or repurpose error code values. +/** Numeric codes for every named RPC error the XRPL node can return. + * + * Values are used as machine-readable error identifiers in RPC responses + * (the `error_code` field). They were never formally promised to be stable, + * but real API consumers depend on them, so the range is now treated as + * **append-only**: new codes go at the end, gaps are never filled, and + * retired values are commented out rather than reassigned. + * + * Codes are grouped thematically (general failures, networking, ledger + * state, malformed commands, bad parameters, internal errors) to guide + * maintainers when choosing where a new code belongs. + * + * `RpcLast` must always equal the highest assigned code; the compile-time + * validation in `ErrorCodes.cpp` enforces this and will fail to compile if + * it is not updated when a new code is added. + * + * @note `RpcUnknown` (-1) is returned by `getErrorInfo()` for any code + * that falls outside the range `(RpcSuccess, RpcLast]`. + */ // Protocol-wide, 50+ files // NOLINTNEXTLINE(cppcoreguidelines-use-enum-class) enum ErrorCodeI { // -1 represents codes not listed in this enumeration - RpcUnknown = -1, + RpcUnknown = -1, /**< Sentinel for out-of-range or unrecognised codes. */ - RpcSuccess = 0, + RpcSuccess = 0, /**< No error. */ - RpcBadSyntax = 1, - RpcJsonRpc = 2, - RpcForbidden = 3, + // General failures + RpcBadSyntax = 1, /**< Request could not be parsed as valid JSON-RPC. */ + RpcJsonRpc = 2, /**< JSON-RPC transport-level error. */ + RpcForbidden = 3, /**< Credentials rejected. */ - RpcWrongNetwork = 4, + RpcWrongNetwork = 4, /**< Request arrived on the wrong network. */ // Misc failure // unused 5, - RpcNoPermission = 6, - RpcNoEvents = 7, + RpcNoPermission = 6, /**< Caller lacks permission for this command. */ + RpcNoEvents = 7, /**< Transport does not support event subscriptions. */ // unused 8, - RpcTooBusy = 9, - RpcSlowDown = 10, - RpcHighFee = 11, - RpcNotEnabled = 12, - RpcNotReady = 13, - RpcAmendmentBlocked = 14, + RpcTooBusy = 9, /**< Server load too high to serve the request now. */ + RpcSlowDown = 10, /**< Caller is sending requests too rapidly. */ + RpcHighFee = 11, /**< Current fee exceeds the caller's stated limit. */ + RpcNotEnabled = 12, /**< Feature not enabled in the server's configuration. */ + RpcNotReady = 13, /**< Server is not yet ready to handle this request. */ + RpcAmendmentBlocked = 14, /**< Node needs an upgrade; amendment-blocked. */ // Networking - RpcNoClosed = 15, - RpcNoCurrent = 16, - RpcNoNetwork = 17, - RpcNotSynced = 18, + RpcNoClosed = 15, /**< Closed ledger is unavailable. */ + RpcNoCurrent = 16, /**< Current ledger is unavailable. */ + RpcNoNetwork = 17, /**< Not synced to the network. */ + RpcNotSynced = 18, /**< Not synced to the network. */ // Ledger state - RpcActNotFound = 19, + RpcActNotFound = 19, /**< Specified account does not exist in the ledger. */ // unused 20, - RpcLgrNotFound = 21, - RpcLgrNotValidated = 22, - RpcMasterDisabled = 23, + RpcLgrNotFound = 21, /**< Requested ledger does not exist. */ + RpcLgrNotValidated = 22, /**< Requested ledger exists but has not yet been validated. */ + RpcMasterDisabled = 23, /**< Master key is disabled on this account. */ // unused 24, // unused 25, // unused 26, // unused 27, // unused 28, - RpcTxnNotFound = 29, - RpcInvalidHotwallet = 30, + RpcTxnNotFound = 29, /**< Transaction not found. */ + RpcInvalidHotwallet = 30, /**< Specified hotwallet address is invalid. */ // Malformed command - RpcInvalidParams = 31, - RpcUnknownCommand = 32, - RpcNoPfRequest = 33, + RpcInvalidParams = 31, /**< One or more request parameters are invalid. */ + RpcUnknownCommand = 32, /**< The requested command is not recognised. */ + RpcNoPfRequest = 33, /**< No pathfinding request is currently in progress. */ // Bad parameter // NOT USED DO NOT USE AGAIN rpcACT_BITCOIN = 34, - RpcActMalformed = 35, - RpcAlreadyMultisig = 36, - RpcAlreadySingleSig = 37, + RpcActMalformed = 35, /**< Account address is malformed. */ + RpcAlreadyMultisig = 36, /**< Account is already set up for multi-signing. */ + RpcAlreadySingleSig = 37, /**< Account is already single-signed. */ // unused 38, // unused 39, - RpcBadFeature = 40, - RpcBadIssuer = 41, - RpcBadMarket = 42, - RpcBadSecret = 43, - RpcBadSeed = 44, - RpcChannelMalformed = 45, - RpcChannelAmtMalformed = 46, - RpcCommandMissing = 47, - RpcDstActMalformed = 48, - RpcDstActMissing = 49, - RpcDstActNotFound = 50, - RpcDstAmtMalformed = 51, - RpcDstAmtMissing = 52, - RpcDstIsrMalformed = 53, + RpcBadFeature = 40, /**< Unknown or invalid amendment feature. */ + RpcBadIssuer = 41, /**< Issuer account address is malformed. */ + RpcBadMarket = 42, /**< Requested order-book does not exist. */ + RpcBadSecret = 43, /**< Secret key does not match the specified account. */ + RpcBadSeed = 44, /**< Seed value is disallowed. */ + RpcChannelMalformed = 45, /**< Payment channel identifier is malformed. */ + RpcChannelAmtMalformed = 46, /**< Payment channel amount is malformed. */ + RpcCommandMissing = 47, /**< Request object is missing the command field. */ + RpcDstActMalformed = 48, /**< Destination account address is malformed. */ + RpcDstActMissing = 49, /**< Destination account was not provided. */ + RpcDstActNotFound = 50, /**< Destination account does not exist in the ledger. */ + RpcDstAmtMalformed = 51, /**< Destination amount, currency, or issuer is malformed. */ + RpcDstAmtMissing = 52, /**< Destination amount, currency, or issuer was not provided. */ + RpcDstIsrMalformed = 53, /**< Destination issuer is malformed. */ // unused 54, // unused 55, // unused 56, - RpcLgrIdxsInvalid = 57, - RpcLgrIdxMalformed = 58, + RpcLgrIdxsInvalid = 57, /**< Ledger index range is invalid. */ + RpcLgrIdxMalformed = 58, /**< Individual ledger index is malformed. */ // unused 59, // unused 60, // unused 61, - RpcPublicMalformed = 62, - RpcSigningMalformed = 63, - RpcSendmaxMalformed = 64, - RpcSrcActMalformed = 65, - RpcSrcActMissing = 66, - RpcSrcActNotFound = 67, - RpcDelegateActNotFound = 68, - RpcSrcCurMalformed = 69, - RpcSrcIsrMalformed = 70, - RpcStreamMalformed = 71, - RpcAtxDeprecated = 72, + RpcPublicMalformed = 62, /**< Public key is malformed. */ + RpcSigningMalformed = 63, /**< Transaction signing data is malformed. */ + RpcSendmaxMalformed = 64, /**< SendMax amount is malformed. */ + RpcSrcActMalformed = 65, /**< Source account address is malformed. */ + RpcSrcActMissing = 66, /**< Source account was not provided. */ + RpcSrcActNotFound = 67, /**< Source account does not exist in the ledger. */ + RpcDelegateActNotFound = 68, /**< Delegate account does not exist in the ledger. */ + RpcSrcCurMalformed = 69, /**< Source currency is malformed. */ + RpcSrcIsrMalformed = 70, /**< Source issuer is malformed. */ + RpcStreamMalformed = 71, /**< Subscription stream specification is malformed. */ + RpcAtxDeprecated = 72, /**< Deprecated API endpoint; use the current API. */ // Internal error (should never happen) - RpcInternal = 73, // Generic internal error. - RpcNotImpl = 74, - RpcNotSupported = 75, - RpcBadKeyType = 76, - RpcDbDeserialization = 77, - RpcExcessiveLgrRange = 78, - RpcInvalidLgrRange = 79, - RpcExpiredValidatorList = 80, + RpcInternal = 73, /**< Generic internal server error. */ + RpcNotImpl = 74, /**< Feature not yet implemented. */ + RpcNotSupported = 75, /**< Operation not supported by this server. */ + RpcBadKeyType = 76, /**< Key type is not supported. */ + RpcDbDeserialization = 77, /**< Failed to deserialize an object from the database. */ + RpcExcessiveLgrRange = 78, /**< Requested ledger range exceeds the 1000-ledger limit. */ + RpcInvalidLgrRange = 79, /**< Requested ledger range bounds are logically invalid. */ + RpcExpiredValidatorList = 80, /**< Validator list has expired; node needs an updated UNL. */ // unused = 90, // DEPRECATED. New code must not use this value. - RpcReportingUnsupported = 91, + RpcReportingUnsupported = 91, /**< @deprecated Reporting-mode-only command sent to a non-reporting node. */ - RpcObjectNotFound = 92, + RpcObjectNotFound = 92, /**< Requested ledger object was not found. */ // AMM - RpcIssueMalformed = 93, + RpcIssueMalformed = 93, /**< AMM asset issue specification is malformed. */ // Oracle - RpcOracleMalformed = 94, + RpcOracleMalformed = 94, /**< Oracle request is malformed. */ // deposit_authorized + credentials - RpcBadCredentials = 95, + RpcBadCredentials = 95, /**< Credentials do not exist, are not accepted, or have expired. */ // Simulate - RpcTxSigned = 96, + RpcTxSigned = 96, /**< Simulate rejected a pre-signed transaction. */ // Pathfinding - RpcDomainMalformed = 97, + RpcDomainMalformed = 97, /**< Domain field is malformed. */ // ledger_entry - RpcEntryNotFound = 98, - RpcUnexpectedLedgerType = 99, + RpcEntryNotFound = 98, /**< Requested ledger entry was not found. */ + RpcUnexpectedLedgerType = 99, /**< Ledger entry type does not match the request. */ - RpcLast = RpcUnexpectedLedgerType // rpcLAST should always equal the last code. + RpcLast = RpcUnexpectedLedgerType /**< Sentinel: always equal to the highest assigned code. */ }; -/** Codes returned in the `warnings` array of certain RPC commands. - - These values need to remain stable. -*/ +/** Numeric codes returned in the `warnings` array of certain RPC responses. + * + * Warning codes appear alongside a successful result (not in the top-level + * `error` field) and inform the caller of advisory conditions such as + * imminent amendment blocking or a deprecated field being used. + * + * Values start at 1001 to be clearly distinct from `ErrorCodeI` values and + * must remain **stable** — external implementations such as Clio hardcode + * specific values (notably `WarnRpcFieldsDeprecated = 2004`). + */ // Protocol-wide, 50+ files // NOLINTNEXTLINE(cppcoreguidelines-use-enum-class) enum WarningCodeI { - WarnRpcUnsupportedMajority = 1001, - WarnRpcAmendmentBlocked = 1002, - WarnRpcExpiredValidatorList = 1003, + WarnRpcUnsupportedMajority = 1001, /**< A non-default amendment has gained majority support. */ + WarnRpcAmendmentBlocked = 1002, /**< Node is amendment-blocked and needs an upgrade. */ + WarnRpcExpiredValidatorList = 1003, /**< Validator list has expired. */ // unused = 1004 - WarnRpcFieldsDeprecated = 2004, // xrpld needs to maintain - // compatibility with Clio on this code. + WarnRpcFieldsDeprecated = 2004, /**< Request used one or more deprecated fields. + * @note Value must stay fixed at 2004; Clio hardcodes it. */ }; //------------------------------------------------------------------------------ @@ -167,144 +194,298 @@ enum WarningCodeI { namespace RPC { -/** Maps an rpc error code to its token, default message, and HTTP status. */ +/** Binds an `ErrorCodeI` to its human-readable token, default message, and HTTP status. + * + * Instances are stored in a compile-time-validated array in `ErrorCodes.cpp` + * and returned by reference from `getErrorInfo()`. `Json::StaticString` fields + * hold raw `const char*` pointers to string literals, avoiding heap allocation + * when the token is written into a JSON response. + * + * The `http_status` field drives load-balancer failover semantics: errors that + * indicate a node is temporarily unable to serve (e.g., amendment-blocked, + * too-busy) use 5xx/429 so a proxy can redirect to a healthy peer; client-fault + * errors use 4xx; everything else defaults to 200 for backward compatibility. + */ struct ErrorInfo { - // Default ctor needed to produce an empty std::array during constexpr eval. + /** Default-constructs an unknown-error entry. + * + * Required so that `std::array` can be value-initialised + * during `constexpr` evaluation of the lookup table. + */ constexpr ErrorInfo() : code(RpcUnknown), token("unknown"), message("An unknown error code."), http_status(200) { } + /** Constructs an `ErrorInfo` with HTTP status defaulting to 200. + * + * @param code The `ErrorCodeI` value this entry represents. + * @param token Short machine-readable string token (e.g., `"invalidParams"`). + * @param message Default human-readable error message. + */ constexpr ErrorInfo(ErrorCodeI code, char const* token, char const* message) : code(code), token(token), message(message), http_status(200) { } + /** Constructs an `ErrorInfo` with an explicit HTTP status. + * + * @param code The `ErrorCodeI` value this entry represents. + * @param token Short machine-readable string token. + * @param message Default human-readable error message. + * @param httpStatus HTTP status code returned to clients and load balancers. + */ constexpr ErrorInfo(ErrorCodeI code, char const* token, char const* message, int httpStatus) : code(code), token(token), message(message), http_status(httpStatus) { } - ErrorCodeI code; - json::StaticString token; - json::StaticString message; - int http_status; + ErrorCodeI code; /**< Numeric error code. */ + json::StaticString token; /**< Short machine-readable string token (e.g., `"invalidParams"`). */ + json::StaticString message; /**< Default human-readable error message. */ + int http_status; /**< HTTP status for this error; 200 unless overridden. */ }; -/** Returns an ErrorInfo that reflects the error code. */ +/** Look up the `ErrorInfo` for a given error code. + * + * Performs a single bounds check followed by a direct array subscript — + * O(1) with no hash table or binary search. + * + * @param code The error code to look up. + * @return A `const` reference to the matching `ErrorInfo`, or to an + * internal unknown-error sentinel if @p code is outside the range + * `(RpcSuccess, RpcLast]`. + */ ErrorInfo const& getErrorInfo(ErrorCodeI code); -/** Add or update the json update to reflect the error code. */ +/** Stamp `error`, `error_code`, and `error_message` fields onto a JSON object. + * + * Uses the default message registered for @p code. Any existing values for + * those three fields are overwritten. + * + * @param code The RPC error code whose metadata to inject. + * @param json The JSON object to mutate. + */ /** @{ */ void injectError(ErrorCodeI code, json::Value& json); +/** Stamp `error`, `error_code`, and `error_message` fields onto a JSON object, + * replacing the default message with a caller-supplied string. + * + * The machine-readable `error` token and numeric `error_code` are taken from + * the registry; only `error_message` is overridden, enabling context-specific + * diagnostics (e.g., naming the exact malformed field) while keeping the + * stable fields intact. + * + * @param code The RPC error code whose token and numeric code to inject. + * @param message Context-specific human-readable message. + * @param json The JSON object to mutate. + */ void injectError(ErrorCodeI code, std::string const& message, json::Value& json); /** @} */ -/** Returns a new json object that reflects the error code. */ +/** Construct a fresh JSON error object for the given code. + * + * Convenience wrapper around `injectError` for handlers that build a response + * from scratch rather than annotating an existing object. + * + * @param code The RPC error code. + * @return A new `Json::Value` object with `error`, `error_code`, and + * `error_message` populated from the registry. + */ /** @{ */ json::Value makeError(ErrorCodeI code); + +/** Construct a fresh JSON error object with a caller-supplied message. + * + * @param code The RPC error code. + * @param message Context-specific message written to `error_message`. + * @return A new `Json::Value` object with `error` and `error_code` from + * the registry and `error_message` set to @p message. + */ json::Value makeError(ErrorCodeI code, std::string const& message); /** @} */ -/** Returns a new json object that indicates invalid parameters. */ -/** @{ */ +/** Construct an `rpcINVALID_PARAMS` error object with a caller-supplied message. + * + * Thin wrapper around `makeError(RpcInvalidParams, message)` used by the + * field-error helper family below to avoid repetitive code at every + * parameter-validation site. + * + * @param message Human-readable description of the parameter problem. + * @return A new `Json::Value` error object for `RpcInvalidParams`. + */ inline json::Value makeParamError(std::string const& message) { return makeError(RpcInvalidParams, message); } +/** Format a "missing field" diagnostic string. + * + * @param name The field name that was absent. + * @return The string `"Missing field ''."`. + */ inline std::string missingFieldMessage(std::string const& name) { return "Missing field '" + name + "'."; } +/** Return an `rpcINVALID_PARAMS` error for a missing required field. + * + * @param name The name of the missing field. + * @return A new JSON error object with a "Missing field" message. + */ inline json::Value missingFieldError(std::string const& name) { return makeParamError(missingFieldMessage(name)); } +/** @copydoc missingFieldError(std::string const&) + * + * @param name The name of the missing field as a `Json::StaticString`. + */ inline json::Value missingFieldError(json::StaticString name) { return missingFieldError(std::string(name)); } +/** Format a "field is not an object" diagnostic string. + * + * @param name The field name that was expected to be an object. + * @return The string `"Invalid field '', not object."`. + */ inline std::string objectFieldMessage(std::string const& name) { return "Invalid field '" + name + "', not object."; } +/** Return an `rpcINVALID_PARAMS` error for a field that must be an object. + * + * @param name The name of the field with the wrong type. + * @return A new JSON error object with a "not object" message. + */ inline json::Value objectFieldError(std::string const& name) { return makeParamError(objectFieldMessage(name)); } +/** @copydoc objectFieldError(std::string const&) + * + * @param name The field name as a `Json::StaticString`. + */ inline json::Value objectFieldError(json::StaticString name) { return objectFieldError(std::string(name)); } +/** Format a generic "invalid field" diagnostic string. + * + * @param name The field name that was invalid. + * @return The string `"Invalid field ''."`. + */ inline std::string invalidFieldMessage(std::string const& name) { return "Invalid field '" + name + "'."; } +/** @copydoc invalidFieldMessage(std::string const&) + * + * @param name The field name as a `Json::StaticString`. + */ inline std::string invalidFieldMessage(json::StaticString name) { return invalidFieldMessage(std::string(name)); } +/** Return an `rpcINVALID_PARAMS` error for a field that failed generic validation. + * + * @param name The name of the invalid field. + * @return A new JSON error object with an "Invalid field" message. + */ inline json::Value invalidFieldError(std::string const& name) { return makeParamError(invalidFieldMessage(name)); } +/** @copydoc invalidFieldError(std::string const&) + * + * @param name The field name as a `Json::StaticString`. + */ inline json::Value invalidFieldError(json::StaticString name) { return invalidFieldError(std::string(name)); } +/** Format a "field has wrong type" diagnostic string. + * + * @param name The field name. + * @param type The expected type description (e.g., `"unsigned integer"`). + * @return The string `"Invalid field '', not ."`. + */ inline std::string expectedFieldMessage(std::string const& name, std::string const& type) { return "Invalid field '" + name + "', not " + type + "."; } +/** @copydoc expectedFieldMessage(std::string const&, std::string const&) + * + * @param name The field name as a `Json::StaticString`. + * @param type The expected type description. + */ inline std::string expectedFieldMessage(json::StaticString name, std::string const& type) { return expectedFieldMessage(std::string(name), type); } +/** Return an `rpcINVALID_PARAMS` error for a field whose value has the wrong type. + * + * @param name The name of the field with the wrong type. + * @param type The expected type description (e.g., `"unsigned integer"`). + * @return A new JSON error object with a "not " message. + */ inline json::Value expectedFieldError(std::string const& name, std::string const& type) { return makeParamError(expectedFieldMessage(name, type)); } +/** @copydoc expectedFieldError(std::string const&, std::string const&) + * + * @param name The field name as a `Json::StaticString`. + * @param type The expected type description. + */ inline json::Value expectedFieldError(json::StaticString name, std::string const& type) { return expectedFieldError(std::string(name), type); } +/** Return an `rpcINVALID_PARAMS` error for commands that require a validator node. + * + * Used by the handful of commands (e.g., `validator_info`) that are only + * meaningful when the local node is a validator. + * + * @return A new JSON error object with the message `"not a validator"`. + */ inline json::Value notValidatorError() { @@ -313,17 +494,47 @@ notValidatorError() /** @} */ -/** Returns `true` if the json contains an rpc error specification. */ +/** Return `true` if @p json represents an RPC error response. + * + * The canonical test used throughout the RPC layer to distinguish error + * responses from successful ones. Only the presence of the `"error"` key + * is checked; the specific code is not inspected. + * + * @param json The JSON value to probe. + * @return `true` if @p json is an object containing an `"error"` member. + * @see getErrorInfo() for code-level branching on a known error. + */ bool containsError(json::Value const& json); -/** Returns http status that corresponds to the error code. */ +/** Return the HTTP status integer associated with an error code. + * + * Used by the HTTP transport layer when constructing response headers. + * HTTP status assignments follow load-balancer failover semantics: transient + * server-side conditions (amendment-blocked, too-busy, not-synced) use 5xx + * or 429 so proxies can retry on a healthy peer; client-fault errors use + * 4xx; codes with no explicit assignment default to 200. + * + * @param code The RPC error code. + * @return HTTP status integer (e.g., 200, 400, 403, 503). + */ int errorCodeHttpStatus(ErrorCodeI code); } // namespace RPC -/** Returns a single string with the contents of an RPC error. */ +/** Concatenate the `error` token and `error_message` from a JSON error value. + * + * Convenience helper for producing logging and diagnostic strings from an + * already-constructed RPC error object. + * + * @param jv A `Json::Value` that must contain an RPC error + * (i.e., `RPC::containsError(jv)` is `true`). + * @return The `error` token string concatenated with the `error_message` + * string, with no separator. + * @note An `XRPL_ASSERT` fires in debug builds if @p jv does not contain + * an error, making misuse diagnosable early. + */ std::string rpcErrorString(json::Value const& jv); diff --git a/include/xrpl/protocol/Feature.h b/include/xrpl/protocol/Feature.h index 281e598bf7..5d9c40b83c 100644 --- a/include/xrpl/protocol/Feature.h +++ b/include/xrpl/protocol/Feature.h @@ -64,23 +64,37 @@ namespace xrpl { -// Feature names must not exceed this length (in characters, excluding the null terminator). +/** Maximum allowed length of a feature name in characters, excluding the null terminator. */ static constexpr std::size_t kMAX_FEATURE_NAME_SIZE = 63; -// Reserve this exact feature-name length (in characters/bytes, excluding the null terminator) -// so that a 32-byte uint256 (for example, in WASM or other interop contexts) can be used -// as a compact, fixed-size feature selector without conflicting with human-readable names. + +/** Feature-name length (in bytes, excluding the null terminator) reserved for + * raw `uint256` hash selectors. + * + * A `uint256` is 32 bytes. Allowing a human-readable name that is exactly 32 + * characters long would create an ambiguous namespace collision with compact + * feature selectors used in WASM or other interop contexts. Names of this + * exact length are rejected at compile time by `validFeatureNameSize()`. + */ static constexpr std::size_t kRESERVED_FEATURE_NAME_SIZE = 32; -// Both validFeatureNameSize and validFeatureName are consteval functions that can be used in -// static_asserts to validate feature names at compile time. They are only used inside -// enforceValidFeatureName in Feature.cpp, but are exposed here for testing. The expected -// parameter `auto fn` is a constexpr lambda which returns a const char*, making it available -// for compile-time evaluation. Read more in https://accu.org/journals/overload/30/172/wu/ +/** Validate a feature name's length at compile time. + * + * Returns `true` iff the name produced by `fn` satisfies both: + * - length ≤ `kMAX_FEATURE_NAME_SIZE` (63 characters), and + * - length ≠ `kRESERVED_FEATURE_NAME_SIZE` (32 characters). + * + * The parameter `fn` must be a `constexpr` lambda returning `const char*`, + * which makes the string literal available for compile-time evaluation. + * See https://accu.org/journals/overload/30/172/wu/ for the idiom. + * + * @param fn A `consteval`-compatible nullary callable returning `const char*`. + * @return `true` if the name length is valid, `false` otherwise. + * @note `std::strlen` is not `constexpr`; a manual loop computes the length. + */ consteval auto validFeatureNameSize(auto fn) -> bool { constexpr char const* kN = fn(); - // Note, std::strlen is not constexpr, we need to implement our own here. constexpr std::size_t kLEN = [](auto n) { std::size_t ret = 0; for (auto ptr = n; *ptr != '\0'; ret++, ++ptr) @@ -91,14 +105,22 @@ validFeatureNameSize(auto fn) -> bool kLEN <= kMAX_FEATURE_NAME_SIZE; } +/** Validate that a feature name contains only printable ASCII characters. + * + * Returns `true` iff every character in the name produced by `fn` has value + * ≥ 0x20 and the high bit (0x80) clear. Rejects: + * - Control characters (below 0x20, e.g. `\t`, `\n`). + * - Non-ASCII bytes (high bit set), which appear in UTF-8 multibyte sequences + * and Unicode identifiers that C++ technically permits but that are visually + * confusable with ASCII characters (e.g. Greek Capital Alpha vs. `'A'`). + * + * @param fn A `consteval`-compatible nullary callable returning `const char*`. + * @return `true` if all characters are printable ASCII, `false` otherwise. + */ consteval auto validFeatureName(auto fn) -> bool { constexpr char const* kN = fn(); - // Prevent the use of visually confusable characters and enforce that feature names - // are always valid ASCII. This is needed because C++ allows Unicode identifiers. - // Characters below 0x20 are nonprintable control characters, and characters with the 0x80 bit - // set are non-ASCII (e.g. UTF-8 encoding of Unicode), so both are disallowed. for (auto ptr = kN; *ptr != '\0'; ++ptr) { if (*ptr & 0x80 || *ptr < 0x20) @@ -107,10 +129,48 @@ validFeatureName(auto fn) -> bool return true; } -enum class VoteBehavior : int { Obsolete = -1, DefaultNo = 0, DefaultYes = 1 }; -enum class AmendmentSupport : int { Retired = -1, Supported = 0, Unsupported = 1 }; +/** Controls whether this server votes for an amendment it supports. + * + * Governs the server's default stance during the amendment voting round. + * The winning value for most amendments progresses from `DefaultNo` + * (governance decides timing) to optionally `DefaultYes` (critical fixes), + * and then to `Obsolete` if the amendment is abandoned without activating. + */ +enum class VoteBehavior : int { + Obsolete = -1, /**< Amendment supported but no longer voted for; retained + * for ledger compatibility only. */ + DefaultNo = 0, /**< Server supports but abstains by default; external + * governance decides when to activate. */ + DefaultYes = 1, /**< Server actively votes for activation; reserved for + * critical bug fixes after off-chain consensus. */ +}; -/** All amendments libxrpl knows about. */ +/** Records how well this build understands a given amendment. + * + * Used by `allAmendments()` to report the full picture of what the server + * knows about each amendment, including retired ones whose conditional code + * has been removed. + */ +enum class AmendmentSupport : int { + Retired = -1, /**< Conditional code removed; amendment remains registered + * so nodes stay amendment-compatible with old ledgers. */ + Supported = 0, /**< Amendment is recognized and the server may vote for it. */ + Unsupported = 1, /**< Amendment is known but this build does not implement it. */ +}; + +/** Return every amendment this build has ever known about, including retired ones. + * + * Maps each amendment's string name to its `AmendmentSupport` status: + * `Supported` (recognized and votable), `Unsupported` (declared but not + * implemented by this build), or `Retired` (conditional code removed, + * retained for ledger compatibility). The returned reference is stable for + * the process lifetime. + * + * @return A sorted map of amendment name → `AmendmentSupport`. + * @note This function must only be called after static initialization + * completes. Calling it during static initialization of another + * translation unit risks querying before the registry is sealed. + */ std::map const& allAmendments(); @@ -132,10 +192,16 @@ namespace detail { #define XRPL_RETIRE_FIX(name) +1 // NOLINTEND(bugprone-macro-parentheses) -// This value SHOULD be equal to the number of amendments registered in -// Feature.cpp. Because it's only used to reserve storage, and determine how -// large to make the FeatureBitset, it MAY be larger. It MUST NOT be less than -// the actual number of amendments. A LogicError on startup will verify this. +/** Compile-time upper bound on the total number of registered amendments. + * + * Used as the `std::bitset` template parameter for `FeatureBitset`. SHOULD + * equal the actual count of entries in `features.macro`, but MAY be larger + * (reserving headroom for future additions). MUST NOT be less than the actual + * count — a `LogicError` on startup verifies this. + * + * @note This is a ceiling, not an exact count. Do not use it as an iteration + * bound or to infer the number of active amendments. + */ static constexpr std::size_t kNUM_FEATURES = (0 + #include @@ -150,40 +216,110 @@ static constexpr std::size_t kNUM_FEATURES = #undef XRPL_FEATURE #pragma pop_macro("XRPL_FEATURE") -/** Amendments that this server supports and the default voting behavior. - Whether they are enabled depends on the Rules defined in the validated - ledger */ +/** Return amendments this build supports and their default vote stance. + * + * Maps each supported amendment's name to its `VoteBehavior`. An amendment + * appearing here is recognized by this build; whether it is actually active + * depends on the `Rules` derived from the validated ledger's Amendments + * object. Retired amendments (`VoteBehavior::Obsolete`) appear here but are + * not voted for. + * + * @return A sorted map of amendment name → `VoteBehavior`. + */ std::map const& supportedAmendments(); -/** Amendments that this server won't vote for by default. - - This function is only used in unit tests. -*/ +/** Return the count of supported amendments this server will NOT vote for. + * + * Includes both `VoteBehavior::DefaultNo` and `VoteBehavior::Obsolete` + * entries. Used in unit tests to verify the vote-tally invariant: + * `numDownVotedAmendments() + numUpVotedAmendments() == supportedAmendments().size()`. + * + * @return Count of amendments this server abstains from or treats as obsolete. + */ std::size_t numDownVotedAmendments(); -/** Amendments that this server will vote for by default. - - This function is only used in unit tests. -*/ +/** Return the count of supported amendments this server will vote for. + * + * Counts only `VoteBehavior::DefaultYes` entries. Used in unit tests to + * verify the vote-tally invariant alongside `numDownVotedAmendments()`. + * + * @return Count of amendments this server actively votes to activate. + */ std::size_t numUpVotedAmendments(); } // namespace detail +/** Look up a registered amendment by name and return its on-chain identifier. + * + * @param name The amendment's string name (e.g. `"Checks"`). + * @return The `uint256` hash computed as `sha512Half(name)`, or `std::nullopt` + * if no amendment with that name has been registered. + * @note Feature names are case-sensitive. Querying an unknown name returns + * `nullopt`; it does not throw. + */ std::optional getRegisteredFeature(std::string const& name); +/** Translate an amendment's `uint256` identifier to its `FeatureBitset` bit position. + * + * This is the hot-path translation used by every `FeatureBitset` operation. + * The result is stable for the process lifetime because the registry is sealed + * before any calls can be made. + * + * @param f A registered amendment identifier. + * @return The zero-based bit index within `FeatureBitset`. + * @throws LogicError if `f` is not a registered amendment. + */ size_t featureToBitsetIndex(uint256 const& f); +/** Translate a `FeatureBitset` bit position back to the amendment's `uint256`. + * + * Inverse of `featureToBitsetIndex()`. Used by `foreachFeature()` to convert + * set bits back into identifiers for callers. + * + * @param i A zero-based bit index within `FeatureBitset`. + * @return The `uint256` hash of the amendment registered at that position. + * @throws LogicError if `i` is out of bounds (≥ the number of registered amendments). + */ uint256 bitsetIndexToFeature(size_t i); +/** Return the human-readable name for an amendment, or its hex representation. + * + * Useful for diagnostics and logging when a `uint256` amendment ID needs to be + * displayed. + * + * @param f The amendment identifier to look up. + * @return The registered string name (e.g. `"Checks"`), or `to_string(f)` if + * `f` is not in the registry. + */ std::string featureToName(uint256 const& f); +/** A set of active amendments, represented as a bitset indexed by amendment ID. + * + * Wraps `std::bitset` and replaces integer-index access + * with `uint256`-based access. Externally every amendment is a `uint256` hash; + * internally `featureToBitsetIndex()` maps it to a compact sequential bit + * position, so all set operations run in O(1). + * + * The full suite of bitwise operators is provided for set algebra: + * - `operator&` — intersection (features enabled in both sets) + * - `operator|` — union (features enabled in either set) + * - `operator^` — symmetric difference + * - `operator-` — **set difference** (`lhs & ~rhs`), used in amendment voting + * to compute "amendments I support that are not yet enabled" + * + * Overloads accepting a bare `uint256` on either side construct a temporary + * single-element `FeatureBitset` for the operation. + * + * @see foreachFeature() to iterate all set bits. + * @see Rules::enabled() for the per-transaction query path. + */ class FeatureBitset : private std::bitset { using base = std::bitset; @@ -215,13 +351,30 @@ public: using base::to_ullong; using base::to_ulong; + /** Construct an empty feature set (no amendments enabled). */ FeatureBitset() = default; + /** Construct from a raw `std::bitset`, asserting no bits are lost. + * + * @param b A bitset whose bit layout matches the amendment registry's + * insertion order. Intended for internal use (e.g. bitwise operators). + */ explicit FeatureBitset(base const& b) : base(b) { XRPL_ASSERT(b.count() == count(), "xrpl::FeatureBitset::FeatureBitset(base) : count match"); } + /** Construct from one or more amendment identifiers. + * + * Each `uint256` is translated to its bitset position via + * `featureToBitsetIndex()`. Asserts that all supplied features are + * distinct (the resulting count equals the number of arguments). + * + * @param f First amendment identifier. + * @param fs Additional amendment identifiers (variadic). + * @throws LogicError (via `featureToBitsetIndex`) if any identifier is + * not registered. + */ template explicit FeatureBitset(uint256 const& f, Fs&&... fs) { @@ -232,6 +385,16 @@ public: "sizeof... do match"); } + /** Construct from any range of `uint256` amendment identifiers. + * + * Iterates `fs` and sets the corresponding bit for each element. + * Asserts that the resulting popcount equals `fs.size()` (all distinct). + * + * @tparam Col A range whose elements are convertible to `uint256`. + * @param fs A collection of amendment identifiers. + * @throws LogicError (via `featureToBitsetIndex`) if any identifier is + * not registered. + */ template explicit FeatureBitset(Col const& fs) { @@ -243,18 +406,35 @@ public: "size do match"); } + /** Return a reference to the bit corresponding to amendment `f`. + * + * @param f A registered amendment identifier. + * @throws LogicError if `f` is not registered. + */ auto operator[](uint256 const& f) { return base::operator[](featureToBitsetIndex(f)); } + /** Return the value of the bit corresponding to amendment `f`. + * + * @param f A registered amendment identifier. + * @throws LogicError if `f` is not registered. + */ auto operator[](uint256 const& f) const { return base::operator[](featureToBitsetIndex(f)); } + /** Set (or clear) the bit for amendment `f`. + * + * @param f A registered amendment identifier. + * @param value `true` to enable the amendment, `false` to disable. + * @return `*this`, for chaining. + * @throws LogicError if `f` is not registered. + */ FeatureBitset& set(uint256 const& f, bool value = true) { @@ -262,6 +442,12 @@ public: return *this; } + /** Clear the bit for amendment `f`. + * + * @param f A registered amendment identifier. + * @return `*this`, for chaining. + * @throws LogicError if `f` is not registered. + */ FeatureBitset& reset(uint256 const& f) { @@ -269,6 +455,12 @@ public: return *this; } + /** Toggle the bit for amendment `f`. + * + * @param f A registered amendment identifier. + * @return `*this`, for chaining. + * @throws LogicError if `f` is not registered. + */ FeatureBitset& flip(uint256 const& f) { @@ -276,6 +468,7 @@ public: return *this; } + /** Intersect this set with `rhs` in-place. */ FeatureBitset& operator&=(FeatureBitset const& rhs) { @@ -283,6 +476,7 @@ public: return *this; } + /** Union this set with `rhs` in-place. */ FeatureBitset& operator|=(FeatureBitset const& rhs) { @@ -290,79 +484,95 @@ public: return *this; } + /** Return the complement: every registered amendment NOT in this set. */ FeatureBitset operator~() const { return FeatureBitset{base::operator~()}; } + /** Return the intersection of two feature sets. */ friend FeatureBitset operator&(FeatureBitset const& lhs, FeatureBitset const& rhs) { return FeatureBitset{static_cast(lhs) & static_cast(rhs)}; } + /** Return the intersection of a feature set and a single amendment. */ friend FeatureBitset operator&(FeatureBitset const& lhs, uint256 const& rhs) { return lhs & FeatureBitset{rhs}; } + /** Return the intersection of a single amendment and a feature set. */ friend FeatureBitset operator&(uint256 const& lhs, FeatureBitset const& rhs) { return FeatureBitset{lhs} & rhs; } + /** Return the union of two feature sets. */ friend FeatureBitset operator|(FeatureBitset const& lhs, FeatureBitset const& rhs) { return FeatureBitset{static_cast(lhs) | static_cast(rhs)}; } + /** Return the union of a feature set and a single amendment. */ friend FeatureBitset operator|(FeatureBitset const& lhs, uint256 const& rhs) { return lhs | FeatureBitset{rhs}; } + /** Return the union of a single amendment and a feature set. */ friend FeatureBitset operator|(uint256 const& lhs, FeatureBitset const& rhs) { return FeatureBitset{lhs} | rhs; } + /** Return the symmetric difference of two feature sets. */ friend FeatureBitset operator^(FeatureBitset const& lhs, FeatureBitset const& rhs) { return FeatureBitset{static_cast(lhs) ^ static_cast(rhs)}; } + /** Return the symmetric difference of a feature set and a single amendment. */ friend FeatureBitset operator^(FeatureBitset const& lhs, uint256 const& rhs) { return lhs ^ FeatureBitset{rhs}; } + /** Return the symmetric difference of a single amendment and a feature set. */ friend FeatureBitset operator^(uint256 const& lhs, FeatureBitset const& rhs) { return FeatureBitset{lhs} ^ rhs; } - // set difference + /** Return the set difference: amendments in `lhs` that are not in `rhs` (`lhs & ~rhs`). + * + * Used in amendment voting to compute "amendments this server supports + * that have not yet been enabled on the network". + */ friend FeatureBitset operator-(FeatureBitset const& lhs, FeatureBitset const& rhs) { return lhs & ~rhs; } + /** Return the set difference of a feature set minus a single amendment. */ friend FeatureBitset operator-(FeatureBitset const& lhs, uint256 const& rhs) { return lhs - FeatureBitset{rhs}; } + /** Return the set difference: a single amendment minus all amendments in `rhs`. */ friend FeatureBitset operator-(uint256 const& lhs, FeatureBitset const& rhs) { @@ -370,6 +580,16 @@ public: } }; +/** Invoke a callback for each amendment enabled in `bs`. + * + * Iterates all bit positions in `bs`, translates each set bit back to its + * `uint256` amendment identifier via `bitsetIndexToFeature()`, and passes it + * to `f`. Unset bits are skipped. + * + * @tparam F A callable accepting a single `uint256 const&` argument. + * @param bs The feature set to iterate. + * @param f Callback invoked once per enabled amendment. + */ template void foreachFeature(FeatureBitset bs, F&& f) @@ -381,6 +601,13 @@ foreachFeature(FeatureBitset bs, F&& f) } } +// --- Amendment identifier declarations --- +// +// A second X-macro pass over features.macro declares one `extern uint256 const` +// variable per active amendment (e.g. `featureChecks`, `fixAMMOverflowOffer`). +// These are the identifiers used throughout the codebase in +// `rules.enabled(featureName)` calls. Retired entries expand to nothing because +// their conditional code has been removed. #pragma push_macro("XRPL_FEATURE") #undef XRPL_FEATURE #pragma push_macro("XRPL_FIX") diff --git a/include/xrpl/protocol/Fees.h b/include/xrpl/protocol/Fees.h index c94ba31b8a..5c41c92365 100644 --- a/include/xrpl/protocol/Fees.h +++ b/include/xrpl/protocol/Fees.h @@ -4,24 +4,56 @@ namespace xrpl { -// Deprecated constant for backwards compatibility with pre-XRPFees amendment. -// This was the reference fee units used in the old fee calculation. +/** Reference fee cost in abstract "fee units" used before the XRPFees amendment. + * + * Prior to `featureXRPFees`, transaction costs were expressed in fee units + * rather than drops; a reference transaction cost 10 fee units and the actual + * drop cost was determined by multiplying by a per-ledger scaling factor. + * After the amendment, fees are expressed natively in drops via `XRPAmount`. + * + * This constant is retained as a compatibility shim: it is written into + * `sfReferenceFeeUnits` in validation objects and `fee_ref` in JSON + * subscription messages when `featureXRPFees` is not active, preserving the + * legacy wire format consumed by older clients. + */ inline constexpr std::uint32_t kFEE_UNITS_DEPRECATED = 10; -/** Reflects the fee settings for a particular ledger. - - The fees are always the same for any transactions applied - to a ledger. Changes to fees occur in between ledgers. -*/ +/** Snapshot of a ledger's fee schedule. + * + * Packages the three economically significant fee parameters — transaction + * cost, base account reserve, and per-object reserve increment — into a + * single value-semantic aggregate. Obtained via `ReadView::fees()` so that + * transactors, preflight checks, and RPC handlers can query fee parameters + * without knowing the concrete view type. + * + * @invariant Fee parameters are constant within a ledger; changes take + * effect only at the next ledger boundary and are driven by validator + * fee-vote consensus updating the `FeeSettings` SLE. + */ struct Fees { - /** @brief Cost of a reference transaction in drops. */ + /** Minimum fee for a reference transaction, in drops. + * + * Transactions paying fewer drops than this value are rejected. + * Zero-initialized so that a default-constructed `Fees` acts as a + * safe placeholder in tests or before a ledger is loaded. + */ XRPAmount base{0}; - /** @brief Minimum XRP an account must hold to exist on the ledger. */ + /** Minimum XRP balance every account must hold simply to exist, in drops. + * + * An account whose balance falls below its total reserve (see + * `accountReserve()`) becomes reserve-deficient and cannot send payments. + */ XRPAmount reserve{0}; - /** @brief Additional XRP reserve required per owned ledger object. */ + /** Additional reserve required for each ledger object owned by an account, + * in drops. + * + * Applies to trust lines, offers, escrows, NFT tokens, and other objects + * that consume shared ledger state. Multiplied by `ownerCount` in + * `accountReserve()` to produce the total per-object reserve charge. + */ XRPAmount increment{0}; explicit Fees() = default; @@ -29,16 +61,31 @@ struct Fees Fees& operator=(Fees const&) = default; + /** Construct a fee schedule from explicit drop amounts. + * + * @param base Minimum fee for a reference transaction, in drops. + * @param reserve Base account reserve, in drops. + * @param increment Per-owned-object reserve increment, in drops. + */ Fees(XRPAmount base, XRPAmount reserve, XRPAmount increment) : base(base), reserve(reserve), increment(increment) { } - /** Returns the account reserve given the owner count, in drops. - - The reserve is calculated as the reserve base plus - the reserve increment times the number of increments. - */ + /** Compute the total XRP reserve an account must hold, in drops. + * + * Applies the formula `reserve + ownerCount * increment`. Callers + * checking whether an account can afford a *new* object should pass + * `ownerCount + 1` — the post-creation count — so the check accounts + * for the incremental cost of the object being created. + * + * @note Pseudo-accounts (AMM, Vault, LoanBroker) are exempt from + * reserves; their callers bypass this method entirely. + * + * @param ownerCount Number of ledger objects currently owned by the + * account (from `sfOwnerCount` on the `AccountRoot` SLE). + * @return Total required balance in drops. + */ [[nodiscard]] XRPAmount accountReserve(std::size_t ownerCount) const { diff --git a/include/xrpl/protocol/HashPrefix.h b/include/xrpl/protocol/HashPrefix.h index 1b05d450a1..c9a6a86e37 100644 --- a/include/xrpl/protocol/HashPrefix.h +++ b/include/xrpl/protocol/HashPrefix.h @@ -1,3 +1,12 @@ +/** @file + * Protocol hash domain separation via 4-byte prefixes. + * + * Every XRPL hashing context prepends a `HashPrefix` constant to its input + * so that two structurally different objects that share identical serialized + * bytes can never collide in hash space. See `HashPrefix` for the full list + * of contexts and `hash_append` for the N3980-compatible integration point. + */ + #pragma once #include @@ -8,6 +17,19 @@ namespace xrpl { namespace detail { +/** Pack three ASCII characters into the high 24 bits of a `uint32_t`. + * + * The resulting value has the form `(a << 24) | (b << 16) | (c << 8)`, + * leaving the low byte as zero. The trailing zero acts as an implicit + * separator and prevents any prefix from coinciding with a valid 1- or + * 2-byte byte sequence. The ASCII mnemonics make prefixes self-documenting + * in hex dumps (e.g. `TransactionId` appears as `0x54584E00`, i.e. `TXN\0`). + * + * @param a First character of the 3-letter mnemonic. + * @param b Second character of the 3-letter mnemonic. + * @param c Third character of the 3-letter mnemonic. + * @return A `constexpr` `uint32_t` suitable for use as a `HashPrefix` value. + */ constexpr std::uint32_t makeHashPrefix(char a, char b, char c) { @@ -17,58 +39,121 @@ makeHashPrefix(char a, char b, char c) } // namespace detail -/** Prefix for hashing functions. - - These prefixes are inserted before the source material used to generate - various hashes. This is done to put each hash in its own "space." This way, - two different types of objects with the same binary data will produce - different hashes. - - Each prefix is a 4-byte value with the last byte set to zero and the first - three bytes formed from the ASCII equivalent of some arbitrary string. For - example "TXN". - - @note Hash prefixes are part of the protocol; you cannot, arbitrarily, - change the type or the value of any of these without causing breakage. -*/ +/** 4-byte domain-separation sentinels prepended to every XRPL hash input. + * + * Each enumerator identifies a distinct hashing context. Prepending the + * prefix ensures that two objects from different contexts with byte-for-byte + * identical serializations always produce different digests, closing a class + * of hash-collision attacks at the protocol layer. + * + * The prefix is consumed in one of two ways depending on the call site: + * - **Serializer prefix** (`s.add32(HashPrefix::TxSign)`): writes the raw + * `uint32_t` into a `Serializer` buffer before appending signing fields. + * - **`hash_append` composition** (`hash_append(h, HashPrefix::InnerNode)`): + * feeds the 4-byte value directly into a streaming hasher, avoiding an + * intermediate buffer. + * + * Both produce the same 4-byte prefix at position zero of the hash input. + * + * @note Hash prefixes are protocol-immutable. Changing the mnemonic letters + * or the numeric value of any enumerator breaks consensus and cross-node + * compatibility irreversibly. + */ enum class HashPrefix : std::uint32_t { - /** transaction plus signature to give transaction ID */ + /** Canonical transaction ID: SHA-512/2 of `TXN\0` followed by the + * transaction bytes including its signature field. + * Distinct from `TxSign` (which excludes the signature) so that signing + * payloads and transaction IDs operate in separate hash namespaces. + */ TransactionId = detail::makeHashPrefix('T', 'X', 'N'), - /** transaction plus metadata */ + /** Transaction-plus-metadata leaf node in the transaction SHAMap + * (`SND\0`). Used by `SHAMapTxPlusMetaLeafNode` to hash a transaction + * together with its execution metadata. Distinct from `TransactionId` + * so a raw transaction and its annotated form can never collide. + */ TxNode = detail::makeHashPrefix('S', 'N', 'D'), - /** account state */ + /** Account-state leaf node in the SHAMap (`MLN\0`). Used by + * `SHAMapAccountStateLeafNode` when computing or verifying the hash of + * a single ledger-state entry. + */ LeafNode = detail::makeHashPrefix('M', 'L', 'N'), - /** inner node in V1 tree */ + /** SHAMap inner (branch) node (`MIN\0`). Used by `SHAMapInnerNode` + * when hashing the 16 child-hash slots of a branch node. Distinct from + * `LeafNode` so inner-node hashes never collide with leaf-node hashes. + */ InnerNode = detail::makeHashPrefix('M', 'I', 'N'), - /** ledger master data for signing */ + /** Ledger header signing payload (`LWR\0`). Prepended to the serialized + * ledger header before computing the ledger hash that validators sign and + * that serves as the canonical ledger identifier. + */ LedgerMaster = detail::makeHashPrefix('L', 'W', 'R'), - /** inner transaction to sign */ + /** Single-signature transaction signing payload (`STX\0`). Prepended to + * the serialized transaction body (with signing fields, without the + * signature itself) before a regular key or master key signs. A + * `TxSign` blob cannot be replayed as a `TxMultiSign` contribution + * because the two prefixes produce different digests. + */ TxSign = detail::makeHashPrefix('S', 'T', 'X'), - /** inner transaction to multi-sign */ + /** Multi-signature transaction signing payload (`SMT\0`). Prepended to + * the serialized transaction body plus the signer's `AccountID` suffix + * before each individual signer's key signs. Distinct from `TxSign` to + * prevent a single-sig blob from being replayed as a multi-sig share. + */ TxMultiSign = detail::makeHashPrefix('S', 'M', 'T'), - /** validation for signing */ + /** Validator validation message signing payload (`VAL\0`). Used by + * `STValidation::getSigningHash` to produce the digest that a validator + * signs when asserting agreement on a ledger. + */ Validation = detail::makeHashPrefix('V', 'A', 'L'), - /** proposal for signing */ + /** Consensus proposal signing payload (`PRP\0`). Used by + * `ConsensusProposal` and `RCLCxPeerPos` when signing or verifying a + * peer's position on a candidate ledger during the consensus round. + */ Proposal = detail::makeHashPrefix('P', 'R', 'P'), - /** Manifest */ + /** Validator manifest signing payload (`MAN\0`). Used by the manifest + * system to sign and verify the binding between a validator's master key + * and its rotating ephemeral signing key. + */ Manifest = detail::makeHashPrefix('M', 'A', 'N'), - /** Payment Channel Claim */ + /** Off-ledger payment channel claim payload (`CLM\0`). Prepended to the + * channel ID and authorized amount before the channel owner signs an + * off-ledger claim that a counterparty can later submit on-chain. + */ PaymentChannelClaim = detail::makeHashPrefix('C', 'L', 'M'), - /** Batch */ + /** Batch transaction signing payload (`BCH\0`). Prepended to the outer + * batch flags, inner transaction count, and list of inner transaction + * IDs before signing a batch. See `serializeBatch()` in `Batch.h`. + */ Batch = detail::makeHashPrefix('B', 'C', 'H'), }; +/** Feed a `HashPrefix` into a N3980-compatible streaming hasher. + * + * Casts the prefix to its underlying `uint32_t` representation and forwards + * it to `beast::hash_append`, allowing a `HashPrefix` to be composed with + * other arguments in a single variadic `sha512Half` call: + * @code + * sha512Half(HashPrefix::transactionID, data) + * @endcode + * No temporary allocation or explicit serialization step is required; the + * 4-byte prefix is fed directly into the running digest state. + * + * @tparam Hasher A type satisfying the N3980 `hash_append` protocol + * (e.g. `sha512_half_hasher`). + * @param h The hasher instance to update. + * @param hp The prefix value to append. + */ template void hash_append(Hasher& h, HashPrefix const& hp) noexcept diff --git a/include/xrpl/protocol/IOUAmount.h b/include/xrpl/protocol/IOUAmount.h index 6ce773fabd..0e07062d72 100644 --- a/include/xrpl/protocol/IOUAmount.h +++ b/include/xrpl/protocol/IOUAmount.h @@ -11,16 +11,35 @@ namespace xrpl { -/** Floating point representation of amounts with high dynamic range - - Amounts are stored as a normalized signed mantissa and an exponent. The - range of the normalized exponent is [-96,80] and the range of the absolute - value of the normalized mantissa is [1000000000000000, 9999999999999999]. - - Arithmetic operations can throw std::overflow_error during normalization - if the amount exceeds the largest representable amount, but underflows - will silently truncate to zero. -*/ +/** Fixed-precision decimal floating-point type for IOU (non-native) balances. + * + * Encodes a value as `mantissa × 10^exponent` using a 64-bit signed mantissa + * and an integer exponent. Canonical form requires the absolute value of the + * mantissa to lie in `[10^15, 10^16−1]` (i.e., `[1000000000000000, + * 9999999999999999]`) and the exponent to lie in `[-96, 80]`. These bounds + * match the on-wire limits in `STAmount`, so a normalized `IOUAmount` is + * always serializable. + * + * Zero is the sentinel `{mantissa=0, exponent=-100}`. The exponent `-100` is + * chosen to be below the minimum representable non-zero exponent (`-96`), so + * that numeric ordering via the exponent field correctly places zero below + * the smallest positive amount. + * + * Arithmetic operations can throw `std::overflow_error` during normalization + * if the result exceeds the largest representable amount; underflows silently + * truncate to zero. This asymmetry is intentional: overflow indicates a + * programming error, while sub-minimum amounts arise naturally from interest + * calculations and must degrade gracefully. + * + * The class privately inherits `boost::totally_ordered` and + * `boost::additive` to derive the full set of comparison and binary + * arithmetic operators from the handful of hand-written primitives + * (`operator==`, `operator<`, `operator+=`, `operator-`). + * + * @note Normalization has two code paths selected by `getSTNumberSwitchover()`: + * the legacy in-place loop and the modern path delegating to + * `Number::normalizeToRange`. Use `NumberSO` to scope either path. + */ class IOUAmount : private boost::totally_ordered, private boost::additive { private: @@ -30,39 +49,124 @@ private: exponent_type exponent_{}; /** Adjusts the mantissa and exponent to the proper range. - - This can throw if the amount cannot be normalized, or is larger than - the largest value that can be represented as an IOU amount. Amounts - that are too small to be represented normalize to 0. - */ + * + * Scales the mantissa up (multiply by 10, decrement exponent) or down + * (divide by 10, increment exponent) until the absolute value of the + * mantissa is in `[10^15, 10^16−1]` and the exponent is in `[-96, 80]`. + * + * Which algorithm is used depends on `getSTNumberSwitchover()`: when + * false, a legacy digit-by-digit loop; when true (the default), delegates + * to `Number::normalizeToRange`. + * + * @throws std::overflow_error if the value is too large to be represented. + * @note Underflow silently rounds to zero rather than throwing. + */ void normalize(); + /** Convert a `Number` to an `IOUAmount` by fitting its mantissa into + * the IOU `10^15` precision range via `Number::normalizeToRange`. + * + * @param number The `Number` value to convert. + * @return The nearest representable `IOUAmount`. + */ static IOUAmount fromNumber(Number const& number); public: + /** Default-constructs a zero amount (`{mantissa=0, exponent=0}`). + * + * @note The raw fields are zero-initialized but `normalize()` is not + * called; use `IOUAmount{beast::kZERO}` to get the canonical zero + * sentinel `{mantissa=0, exponent=-100}`. + */ IOUAmount() = default; + + /** Construct from a `Number`, fitting its mantissa into IOU precision. + * + * @param other The `Number` to convert. Delegates to `fromNumber()`. + */ explicit IOUAmount(Number const& other); + + /** Construct the canonical zero sentinel `{mantissa=0, exponent=-100}`. */ IOUAmount(beast::Zero); + + /** Construct from raw mantissa and exponent, then normalize. + * + * @param mantissa The signed mantissa; sign determines the amount's sign. + * @param exponent The power-of-ten exponent. + * @throws std::overflow_error if the value cannot be normalized to the + * representable range after scaling. + */ IOUAmount(mantissa_type mantissa, exponent_type exponent); + /** Reset to the canonical zero sentinel `{mantissa=0, exponent=-100}`. + * + * The exponent `-100` ensures zero sorts below the smallest positive + * amount whose minimum exponent is `-96`. + */ IOUAmount& operator=(beast::Zero); + /** Implicit conversion to `Number`. + * + * Constructs `Number{mantissa_, exponent_}`, bridging the legacy IOU + * type into the modern arithmetic layer. Conversion to `Number` is + * intentionally implicit; the reverse (from `Number`) is explicit. + */ operator Number() const; + /** Add another amount in-place. + * + * When `getSTNumberSwitchover()` is true, routes through + * `Number` arithmetic for correct handling across the two normalization + * regimes. Otherwise, performs manual exponent alignment. + * + * @param other The amount to add. + * @return Reference to `*this` after normalization. + * @throws std::overflow_error if the result exceeds the representable range. + */ IOUAmount& operator+=(IOUAmount const& other); + /** Subtract another amount in-place. + * + * Implemented as `*this += -other`. + * + * @param other The amount to subtract. + * @return Reference to `*this` after normalization. + * @throws std::overflow_error if the result exceeds the representable range. + */ IOUAmount& operator-=(IOUAmount const& other); + /** Negate the amount without calling `normalize()`. + * + * Flips the sign of the mantissa. Safe because the negation of a + * normalized value is also normalized; negating zero leaves the + * `{0, -100}` sentinel unchanged. + * + * @return A new `IOUAmount` with the same magnitude and opposite sign. + */ IOUAmount operator-() const; + /** Returns true if both amounts have identical mantissa and exponent. + * + * Valid because every non-zero value has a unique canonical + * representation after normalization, and zero is always `{0, -100}`. + * + * @param other The amount to compare. + */ bool operator==(IOUAmount const& other) const; + /** Returns true if this amount is strictly less than `other`. + * + * Delegates to `Number` comparison, which handles the zero sentinel and + * cross-regime comparisons correctly. + * + * @param other The amount to compare against. + */ bool operator<(IOUAmount const& other) const; @@ -74,12 +178,26 @@ public: [[nodiscard]] int signum() const noexcept; + /** Return the raw (normalized) exponent. + * + * The value is in `[-96, 80]` for non-zero amounts, or `-100` for zero. + */ [[nodiscard]] exponent_type exponent() const noexcept; + /** Return the raw (normalized) signed mantissa. + * + * For non-zero amounts, the absolute value is in `[10^15, 10^16−1]`. + * Zero returns `0`. + */ [[nodiscard]] mantissa_type mantissa() const noexcept; + /** Return the smallest representable positive `IOUAmount`. + * + * Corresponds to `{mantissa = 10^15, exponent = -96}`, the lower-left + * corner of the normalized canonical range. + */ static IOUAmount minPositiveAmount(); @@ -104,8 +222,6 @@ inline IOUAmount::IOUAmount(mantissa_type mantissa, exponent_type exponent) inline IOUAmount& IOUAmount::operator=(beast::Zero) { - // The -100 is used to allow 0 to sort less than small positive values - // which will have a large negative exponent. mantissa_ = 0; exponent_ = -100; return *this; @@ -168,29 +284,73 @@ IOUAmount::mantissa() const noexcept return mantissa_; } +/** Format an `IOUAmount` as a human-readable decimal string. + * + * Produces integer notation when the exponent is non-negative (e.g. `"2e20"` + * for very large values), or decimal notation for fractional amounts (e.g. + * `"0.025"`). Scientific notation is used when the decimal form would be + * impractical. + * + * @param amount The amount to format. + * @return A decimal string representation of `amount`. + */ std::string to_string(IOUAmount const& amount); -/* Return num*amt/den - This function keeps more precision than computing - num*amt, storing the result in an IOUAmount, then - dividing by den. -*/ +/** Compute `amt × num / den` with higher precision than sequential operations. + * + * Intermediate products are held in a 128-bit unsigned integer to avoid + * overflow when multiplying a 64-bit mantissa by a 32-bit numerator. The + * quotient and remainder are then rescaled to fit the 64-bit IOU mantissa + * range, with the rounding direction determined by `roundUp`. + * + * @param amt The base amount to scale. + * @param num Numerator of the scaling ratio; may be zero to produce zero. + * @param den Denominator of the scaling ratio; must not be zero. + * @param roundUp When true, rounds toward positive infinity for positive + * results and toward negative infinity for negative results (directed + * rounding semantics). When false, rounds toward zero. + * @return The scaled amount `amt × num / den`. + * @throws std::overflow_error if the result exceeds the representable range. + * @throws std::domain_error (or similar) if `den` is zero. + */ IOUAmount mulRatio(IOUAmount const& amt, std::uint32_t num, std::uint32_t den, bool roundUp); -// Since many uses of the number class do not have access to a ledger, -// getSTNumberSwitchover needs to be globally accessible. - +/** Return the current coroutine-local STNumber switchover flag. + * + * When true, `IOUAmount::normalize()` and `operator+=` use the modern + * `Number`-based code path; when false, the legacy in-place loop is used. + * The flag is stored in a `LocalValue` so each coroutine has its own + * independent copy. Globally accessible because most callers do not have + * access to a ledger/rules context. + * + * @return true if the `Number` normalization path is active. + * @see setSTNumberSwitchover, NumberSO + */ bool getSTNumberSwitchover(); +/** Set the coroutine-local STNumber switchover flag. + * + * Prefer the `NumberSO` RAII guard for scoped changes. + * + * @param v true to enable the `Number` normalization path; false for legacy. + * @see getSTNumberSwitchover, NumberSO + */ void setSTNumberSwitchover(bool v); -/** RAII class to set and restore the Number switchover. +/** RAII guard that temporarily overrides the coroutine-local STNumber + * switchover flag and restores the previous value on destruction. + * + * Construct with `true` to force the `Number`-based normalization path, or + * `false` to force the legacy loop. Useful in tests and ledger-replay code + * that must exercise a specific path without global side effects. + * + * @note Non-copyable; intended for stack-only use. + * @see getSTNumberSwitchover, setSTNumberSwitchover */ - class NumberSO { bool saved_; diff --git a/include/xrpl/protocol/Indexes.h b/include/xrpl/protocol/Indexes.h index 14e7d95e23..b2455a4795 100644 --- a/include/xrpl/protocol/Indexes.h +++ b/include/xrpl/protocol/Indexes.h @@ -1,3 +1,18 @@ +/** @file + * Single authoritative source for computing the 256-bit ledger-state addresses + * of every object type in the XRP Ledger. + * + * All key derivations use "tagged hashing": a `sha512Half` over a type-specific + * `LedgerNameSpace` discriminator prepended to the object's identifying + * parameters. This prevents cross-type key collisions even when two object + * types share identical parameter values. The namespace discriminators are + * protocol-immutable; changing them constitutes a hard fork. + * + * The primary API is the `xrpl::keylet` namespace, whose functions return + * `Keylet` values pairing a 256-bit key with its expected `LedgerEntryType`. + * Free functions below the namespace (`getBookBase`, `getQuality`, etc.) are + * deprecated predecessors retained for backward compatibility. + */ #pragma once #include @@ -17,84 +32,141 @@ namespace xrpl { class SeqProxy; -/** Keylet computation functions. - Entries in the ledger are located using 256-bit locators. The locators are - calculated using a wide range of parameters specific to the entry whose - locator we are calculating (e.g. an account's locator is derived from the - account's address, whereas the locator for an offer is derived from the - account and the offer sequence.) - - To enhance type safety during lookup and make the code more robust, we use - keylets, which contain not only the locator of the object but also the type - of the object being referenced. - - These functions each return a type-specific keylet. -*/ +/** Keylet computation functions for every XRPL ledger object type. + * + * Entries in the ledger are located using 256-bit keys derived by hashing + * object-specific parameters under a type-specific namespace discriminator. + * Each function in this namespace returns a `Keylet` — a pair of the derived + * key and the expected `LedgerEntryType` — enabling type-safe ledger lookups + * that catch category errors at retrieval time via `Keylet::check()`. + * + * @note All namespace discriminator values are part of the consensus protocol + * and must never be changed. Adding a new keylet function requires + * assigning a new, previously unused discriminator character. + */ namespace keylet { -/** AccountID root */ +/** Return the keylet for an AccountRoot ledger entry. + * + * @param id The account address. + * @return Keylet typed `ltACCOUNT_ROOT`. + */ Keylet account(AccountID const& id) noexcept; -/** The index of the amendment table */ +/** Return the keylet for the singleton amendments table. + * + * The amendments object has no parameters; its key is computed once and + * returned as a reference to a function-local static (Meyers singleton). + * + * @return Reference to a static `Keylet` typed `ltAMENDMENTS`. + */ Keylet const& amendments() noexcept; -/** Any item that can be in an owner dir. */ +/** Return a wildcard keylet for any item that can appear in an owner directory. + * + * Uses `ltCHILD` so that `Keylet::check()` accepts any entry type — + * useful when iterating a directory without knowing the contained type. + * + * @param key Raw 256-bit ledger key of the directory child entry. + * @return Keylet typed `ltCHILD`. + */ Keylet child(uint256 const& key) noexcept; -/** The index of the "short" skip list - - The "short" skip list is a node (at a fixed index) that holds the hashes - of ledgers since the last flag ledger. It will contain, at most, 256 hashes. -*/ +/** Return the keylet for the "short" ledger-hash skip list. + * + * The short skip list is a singleton object holding the hashes of ledgers + * since the last flag ledger (at most 256 entries). Its key is computed + * once and returned as a reference to a function-local static. + * + * @return Reference to a static `Keylet` typed `ltLEDGER_HASHES`. + */ Keylet const& skip() noexcept; -/** The index of the long skip for a particular ledger range. - - The "long" skip list is a node that holds the hashes of (up to) 256 flag - ledgers. - - It can be used to efficiently skip back to any ledger using only two hops: - the first hop gets the "long" skip list for the ledger it wants to retrieve - and uses it to get the hash of the flag ledger whose short skip list will - contain the hash of the requested ledger. -*/ +/** Return the keylet for a "long" ledger-hash skip list page. + * + * Each long skip list page stores hashes of up to 256 flag ledgers within + * a 65536-ledger range. Together with the short skip list, any historical + * ledger can be located in at most two hops: one to the long skip list for + * the target range, one to the short skip list around the target ledger. + * + * @param ledger Any ledger index within the desired 65536-ledger range; + * only the upper 16 bits determine the page key. + * @return Keylet typed `ltLEDGER_HASHES` for the corresponding skip-list page. + */ Keylet skip(LedgerIndex ledger) noexcept; -/** The (fixed) index of the object containing the ledger fees. */ +/** Return the keylet for the singleton fee-settings object. + * + * Its key is computed once and returned as a reference to a function-local + * static (Meyers singleton). + * + * @return Reference to a static `Keylet` typed `ltFEE_SETTINGS`. + */ Keylet const& fees() noexcept; -/** The (fixed) index of the object containing the ledger negativeUNL. */ +/** Return the keylet for the singleton negative-UNL object. + * + * Its key is computed once and returned as a reference to a function-local + * static (Meyers singleton). + * + * @return Reference to a static `Keylet` typed `ltNEGATIVE_UNL`. + */ Keylet const& negativeUNL() noexcept; -/** The beginning of an order book */ +/** Functor that returns the root keylet for an order book directory. + * + * The returned keylet encodes quality 0 in the last 8 bytes of the key, + * making it the floor of the book's range in the SHAMap. Use `kBOOK` + * (the pre-constructed singleton instance) rather than constructing directly. + * + * @see keylet::quality + */ struct BookT { explicit BookT() = default; + /** Return the keylet for the root directory page of @p b. + * + * @param b Order book specifying the in/out asset pair and optional domain. + * @return Keylet typed `ltDIR_NODE` with quality 0 in the last 8 bytes. + */ Keylet operator()(Book const& b) const; }; static BookT const kBOOK{}; -/** The index of a trust line for a given currency - - Note that a trustline is *shared* between two accounts (commonly referred - to as the issuer and the holder); if Alice sets up a trust line to Bob for - BTC, and Bob trusts Alice for BTC, here is only a single BTC trust line - between them. -*/ +/** Return the keylet for a trust line (RippleState) between two accounts. + * + * A trust line is a bilateral ledger object shared by both accounts. The + * two account IDs are sorted before hashing so that `line(Alice, Bob, USD)` + * and `line(Bob, Alice, USD)` produce the same key. + * + * @note `id0 == id1` is permitted (TrustSet may look up and delete malformed + * self-trust lines); the absence of a strict inequality assert is intentional. + * + * @param id0 One account on the trust line. + * @param id1 The other account on the trust line. + * @param currency Currency of the trust line. + * @return Keylet typed `ltRIPPLE_STATE`. + */ /** @{ */ Keylet line(AccountID const& id0, AccountID const& id1, Currency const& currency) noexcept; +/** Return the keylet for the trust line between @p id and the issuer of @p issue. + * + * @param id One of the two accounts on the trust line. + * @param issue Issue whose account and currency identify the trust line. + * @return Keylet typed `ltRIPPLE_STATE`. + */ inline Keylet line(AccountID const& id, Issue const& issue) noexcept { @@ -102,11 +174,21 @@ line(AccountID const& id, Issue const& issue) noexcept } /** @} */ -/** An offer from an account */ +/** Return the keylet for an offer placed by an account. + * + * @param id Account that placed the offer. + * @param seq Sequence number of the OfferCreate transaction. + * @return Keylet typed `ltOFFER`. + */ /** @{ */ Keylet offer(AccountID const& id, std::uint32_t seq) noexcept; +/** Return a typed keylet for an offer from its pre-computed key. + * + * @param key Pre-computed 256-bit offer key. + * @return Keylet typed `ltOFFER`. + */ inline Keylet offer(uint256 const& key) noexcept { @@ -114,31 +196,77 @@ offer(uint256 const& key) noexcept } /** @} */ -/** The initial directory page for a specific quality */ +/** Return the keylet for an order-book directory page at a specific quality. + * + * Writes @p q as a big-endian 64-bit value into the last 8 bytes of the + * book's base key. Because `uint256` keys sort as big-endian integers in + * the SHAMap, adjacent quality levels occupy adjacent addresses, enabling + * O(1) price-level iteration without a secondary index. + * + * @param k Base keylet for the order book (must be `ltDIR_NODE`). + * @param q 64-bit quality value (inverted exchange rate) to embed. + * @return Keylet typed `ltDIR_NODE` with @p q encoded in the last 8 bytes. + */ Keylet quality(Keylet const& k, std::uint64_t q) noexcept; -/** The directory for the next lower quality */ +/** Functor that advances a book-directory keylet to the next quality level. + * + * Adds a unit to the 64-bit quality field embedded in the last 8 bytes of + * the key, stepping to the directory for the next higher quality tier in + * the same order book. Use `kNEXT` (the pre-constructed singleton instance) + * rather than constructing directly. + * + * @see keylet::quality + */ struct NextT { explicit NextT() = default; + /** Return the keylet for the next quality tier above @p k. + * + * @param k A directory keylet (must be `ltDIR_NODE`) whose last 8 bytes + * encode a quality value. + * @return Keylet typed `ltDIR_NODE` with quality incremented by 1. + */ Keylet operator()(Keylet const& k) const; }; static NextT const kNEXT{}; -/** A ticket belonging to an account */ +/** Functor that computes ticket keylets. + * + * Use `kTICKET` (the pre-constructed singleton instance) rather than + * constructing directly. + */ struct TicketT { explicit TicketT() = default; + /** Return the keylet for a ticket owned by @p id. + * + * @param id Owner of the ticket. + * @param ticketSeq Sequence number consumed when the ticket was created. + * @return Keylet typed `ltTICKET`. + */ Keylet operator()(AccountID const& id, std::uint32_t ticketSeq) const; + /** Return the keylet for a ticket owned by @p id, resolved via a SeqProxy. + * + * @param id Owner of the ticket. + * @param ticketSeq SeqProxy in ticket mode; asserts if it represents a + * plain sequence number. + * @return Keylet typed `ltTICKET`. + */ Keylet operator()(AccountID const& id, SeqProxy ticketSeq) const; + /** Return a typed keylet for a ticket from its pre-computed key. + * + * @param key Pre-computed 256-bit ticket key. + * @return Keylet typed `ltTICKET`. + */ Keylet operator()(uint256 const& key) const { @@ -147,15 +275,29 @@ struct TicketT }; static TicketT const kTICKET{}; -/** A SignerList */ +/** Return the keylet for an account's multi-signature signer list. + * + * @param account Account whose signer list is being addressed. + * @return Keylet typed `ltSIGNER_LIST` for page 0 (the only allocated page). + */ Keylet signers(AccountID const& account) noexcept; -/** A Check */ +/** Return the keylet for a Check issued by an account. + * + * @param id Account that created the check (via CheckCreate). + * @param seq Sequence number of the CheckCreate transaction. + * @return Keylet typed `ltCHECK`. + */ /** @{ */ Keylet check(AccountID const& id, std::uint32_t seq) noexcept; +/** Return a typed keylet for a check from its pre-computed key. + * + * @param key Pre-computed 256-bit check key. + * @return Keylet typed `ltCHECK`. + */ inline Keylet check(uint256 const& key) noexcept { @@ -163,16 +305,45 @@ check(uint256 const& key) noexcept } /** @} */ -/** A DepositPreauth */ +/** Return the keylet for a deposit pre-authorization record. + * + * Two overloads exist for the two pre-authorization modes — account-to-account + * and credential-set — which hash under distinct namespace discriminators to + * prevent key collisions even when the `owner` is identical. + */ /** @{ */ +/** Return the keylet for a single-account deposit pre-authorization. + * + * @param owner Account granting the pre-authorization. + * @param preauthorized Account being pre-authorized to deposit. + * @return Keylet typed `ltDEPOSIT_PREAUTH`. + */ Keylet depositPreauth(AccountID const& owner, AccountID const& preauthorized) noexcept; +/** Return the keylet for a credential-set deposit pre-authorization. + * + * Each credential in @p authCreds is hashed individually as + * `sha512Half(issuer, credentialType)`; the resulting hashes are then passed + * to the outer hash under the `DepositPreauthCredentials` namespace, which + * is distinct from the account-to-account `DepositPreauth` namespace. + * Because `authCreds` is a `std::set`, iteration order is deterministic and + * the key is stable regardless of insertion order. + * + * @param owner Account granting the pre-authorization. + * @param authCreds Sorted set of (issuer AccountID, credentialType) pairs. + * @return Keylet typed `ltDEPOSIT_PREAUTH`. + */ Keylet depositPreauth( AccountID const& owner, std::set> const& authCreds) noexcept; +/** Return a typed keylet for a deposit pre-auth entry from its pre-computed key. + * + * @param key Pre-computed 256-bit deposit-preauth key. + * @return Keylet typed `ltDEPOSIT_PREAUTH`. + */ inline Keylet depositPreauth(uint256 const& key) noexcept { @@ -182,19 +353,48 @@ depositPreauth(uint256 const& key) noexcept //------------------------------------------------------------------------------ -/** Any ledger entry */ +/** Return a keylet for any ledger entry without type enforcement. + * + * Uses `ltANY` so `Keylet::check()` accepts any entry type. Intended for + * low-level read paths that need to fetch an entry before its type is known. + * + * @param key Raw 256-bit ledger key. + * @return Keylet typed `ltANY`. + */ Keylet unchecked(uint256 const& key) noexcept; -/** The root page of an account's directory */ +/** Return the keylet for the root page of an account's owner directory. + * + * The owner directory lists all objects owned by the account (offers, trust + * lines, escrows, etc.). Subsequent pages beyond page 0 are keyed via + * `keylet::page`. + * + * @param id Account whose owner directory is being addressed. + * @return Keylet typed `ltDIR_NODE`. + */ Keylet ownerDir(AccountID const& id) noexcept; -/** A page in a directory */ +/** Return the keylet for a specific page within a directory. + * + * Page 0 is stored at the root key itself; pages 1+ are stored at keys + * derived by hashing the root key with the page index. + * + * @param root 256-bit key of the directory's root page. + * @param index Zero-based page index; 0 returns the root key unchanged. + * @return Keylet typed `ltDIR_NODE`. + */ /** @{ */ Keylet page(uint256 const& root, std::uint64_t index = 0) noexcept; +/** Return the keylet for a specific page within a directory, from a root keylet. + * + * @param root Keylet of the directory's root page (must be `ltDIR_NODE`). + * @param index Zero-based page index. + * @return Keylet typed `ltDIR_NODE`. + */ inline Keylet page(Keylet const& root, std::uint64_t index = 0) noexcept { @@ -203,176 +403,477 @@ page(Keylet const& root, std::uint64_t index = 0) noexcept } /** @} */ -/** An escrow entry */ +/** Return the keylet for an escrow conditional payment. + * + * @param src Account that created the escrow. + * @param seq Sequence number of the EscrowCreate transaction. + * @return Keylet typed `ltESCROW`. + */ Keylet escrow(AccountID const& src, std::uint32_t seq) noexcept; -/** A PaymentChannel */ +/** Return the keylet for an XRP payment channel. + * + * @param src Funding (source) account. + * @param dst Receiving (destination) account. + * @param seq Sequence number of the PaymentChannelCreate transaction. + * @return Keylet typed `ltPAYCHAN`. + */ Keylet payChan(AccountID const& src, AccountID const& dst, std::uint32_t seq) noexcept; -/** NFT page keylets - - Unlike objects whose ledger identifiers are produced by hashing data, - NFT page identifiers are composite identifiers, consisting of the owner's - 160-bit AccountID, followed by a 96-bit value that determines which NFT - tokens are candidates for that page. +/** NFT page keylets. + * + * Unlike other ledger objects whose keys are produced by hashing, NFT page + * keys are composite values: the high 160 bits hold the owner's `AccountID` + * and the low 96 bits are a range tag derived from an NFToken ID. This + * composite structure enables bounded range scans over all of an owner's NFT + * pages in the SHAMap without a linked-list traversal. */ /** @{ */ -/** A keylet for the owner's first possible NFT page. */ +/** Return the keylet for the owner's lowest possible NFT page (low 96 bits = 0). + * + * This is the floor of the owner's page range. It is normally impossible to + * create an actual NFT page at this key, but it is used in invariant tests + * to exercise the full page range. + * + * @param owner Account that owns the NFT collection. + * @return Keylet typed `ltNFTOKEN_PAGE` with low 96 bits all zero. + */ Keylet nftpageMin(AccountID const& owner); -/** A keylet for the owner's last possible NFT page. */ +/** Return the keylet for the owner's highest possible NFT page (low 96 bits = all ones). + * + * Together with `nftpageMin`, this defines the closed interval covering every + * NFT page belonging to this owner. + * + * @param owner Account that owns the NFT collection. + * @return Keylet typed `ltNFTOKEN_PAGE` with low 96 bits all one. + */ Keylet nftpageMax(AccountID const& owner); +/** Return the keylet for the NFT page that should contain @p token. + * + * Preserves the owner prefix from @p k (high 160 bits) and replaces the + * range tag (low 96 bits) with the corresponding bits of @p token masked + * by `nft::pageMask`. + * + * @param k An NFT page keylet for the same owner (must be `ltNFTOKEN_PAGE`). + * @param token 256-bit NFToken ID whose low 96 bits determine the target page. + * @return Keylet typed `ltNFTOKEN_PAGE` for the page whose range covers @p token. + */ Keylet nftpage(Keylet const& k, uint256 const& token); /** @} */ -/** An offer from an account to buy or sell an NFT */ +/** Return the keylet for an NFToken buy or sell offer. + * + * @param owner Account that created the offer. + * @param seq Sequence number of the NFTokenCreateOffer transaction. + * @return Keylet typed `ltNFTOKEN_OFFER`. + */ +/** @{ */ Keylet nftoffer(AccountID const& owner, std::uint32_t seq); +/** Return a typed keylet for an NFToken offer from its pre-computed key. + * + * @param offer Pre-computed 256-bit NFToken offer key. + * @return Keylet typed `ltNFTOKEN_OFFER`. + */ inline Keylet nftoffer(uint256 const& offer) { return {ltNFTOKEN_OFFER, offer}; } +/** @} */ -/** The directory of buy offers for the specified NFT */ +/** Return the keylet for the directory of buy offers for an NFToken. + * + * @param id 256-bit NFToken ID. + * @return Keylet typed `ltDIR_NODE` for the buy-offer directory. + */ Keylet nftBuys(uint256 const& id) noexcept; -/** The directory of sell offers for the specified NFT */ +/** Return the keylet for the directory of sell offers for an NFToken. + * + * @param id 256-bit NFToken ID. + * @return Keylet typed `ltDIR_NODE` for the sell-offer directory. + */ Keylet nftSells(uint256 const& id) noexcept; -/** AMM entry */ +/** Return the keylet for an AMM pool, keyed by its two pooled assets. + * + * The two assets are sorted via `std::minmax` before hashing, so + * `amm(A, B)` and `amm(B, A)` always produce the same keylet. + * All four combinations of `Issue`/`MPTIssue` asset pairs are supported. + * + * @param issue1 One of the two pooled assets. + * @param issue2 The other pooled asset. + * @return Keylet typed `ltAMM`. + */ +/** @{ */ Keylet amm(Asset const& issue1, Asset const& issue2) noexcept; +/** Return the keylet for an AMM pool from a pre-computed 256-bit AMM ID. + * + * Use this overload when the AMM ID is already available (e.g. stored in + * `sfAMMID` on another SLE) to avoid redundant hashing. + * + * @param amm Pre-computed 256-bit AMM identifier. + * @return Keylet typed `ltAMM`. + */ Keylet amm(uint256 const& amm) noexcept; +/** @} */ -/** A keylet for Delegate object */ +/** Return the keylet for a delegation grant from one account to another. + * + * @param account Account granting delegated authority. + * @param authorizedAccount Account receiving the delegated authority. + * @return Keylet typed `ltDELEGATE`. + */ Keylet delegate(AccountID const& account, AccountID const& authorizedAccount) noexcept; +/** Return the keylet for a cross-chain bridge object. + * + * A door account may host multiple bridges. The key encodes the door + * account and the currency appropriate to @p chainType, ensuring at most + * one bridge per currency per side. + * + * @param bridge Bridge descriptor containing door accounts and issues. + * @param chainType Selects whether to key on the locking or issuing chain side. + * @return Keylet typed `ltBRIDGE`. + */ Keylet bridge(STXChainBridge const& bridge, STXChainBridge::ChainType chainType); -// `seq` is stored as `sfXChainClaimID` in the object +/** Return the keylet for a cross-chain claim ID. + * + * The key encodes the full bridge identity plus the sequential claim ID + * stored as `sfXChainClaimID` in the object. + * + * @param bridge Bridge descriptor. + * @param seq Sequential claim ID (`sfXChainClaimID`). + * @return Keylet typed `ltXCHAIN_OWNED_CLAIM_ID`. + */ Keylet xChainClaimID(STXChainBridge const& bridge, std::uint64_t seq); -// `seq` is stored as `sfXChainAccountCreateCount` in the object +/** Return the keylet for a cross-chain create-account claim ID. + * + * Analogous to `xChainClaimID` but for the create-account workflow. The + * sequential counter is stored as `sfXChainAccountCreateCount` in the object. + * + * @param bridge Bridge descriptor. + * @param seq Sequential create-account claim ID (`sfXChainAccountCreateCount`). + * @return Keylet typed `ltXCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID`. + */ Keylet xChainCreateAccountClaimID(STXChainBridge const& bridge, std::uint64_t seq); +/** Return the keylet for an account's DID (Decentralized Identifier) document. + * + * @param account Account that owns the DID. + * @return Keylet typed `ltDID`. + */ Keylet did(AccountID const& account) noexcept; +/** Return the keylet for a price oracle owned by an account. + * + * An account may own multiple oracles distinguished by unique document IDs. + * + * @param account Account that owns the oracle. + * @param documentID Application-defined identifier distinguishing oracles + * within the same account (`sfOracleDocumentID`). + * @return Keylet typed `ltORACLE`. + */ Keylet oracle(AccountID const& account, std::uint32_t const& documentID) noexcept; +/** Return the keylet for a verifiable credential. + * + * @param subject Account the credential was issued to. + * @param issuer Account that issued the credential. + * @param credType Application-defined credential type byte string. + * @return Keylet typed `ltCREDENTIAL`. + */ +/** @{ */ Keylet credential(AccountID const& subject, AccountID const& issuer, Slice const& credType) noexcept; +/** Return a typed keylet for a credential from its pre-computed key. + * + * @param key Pre-computed 256-bit credential key. + * @return Keylet typed `ltCREDENTIAL`. + */ inline Keylet credential(uint256 const& key) noexcept { return {ltCREDENTIAL, key}; } +/** @} */ +/** Return the keylet for an MPT issuance, identified by sequence and issuer. + * + * Constructs the `MPTID` from @p seq and @p issuer via `makeMptID`, then + * delegates to the `MPTID` overload. + * + * @param seq Issuer's account sequence number at the time of issuance creation. + * @param issuer Account that created the issuance. + * @return Keylet typed `ltMPTOKEN_ISSUANCE`. + */ +/** @{ */ Keylet mptIssuance(std::uint32_t seq, AccountID const& issuer) noexcept; +/** Return the keylet for an MPT issuance from a pre-built MPTID. + * + * @param issuanceID 192-bit MPT issuance identifier (see `makeMptID`). + * @return Keylet typed `ltMPTOKEN_ISSUANCE`. + */ Keylet mptIssuance(MPTID const& issuanceID) noexcept; +/** Return a typed keylet for an MPT issuance from its pre-computed key. + * + * @param issuanceKey Pre-computed 256-bit issuance key. + * @return Keylet typed `ltMPTOKEN_ISSUANCE`. + */ inline Keylet mptIssuance(uint256 const& issuanceKey) { return {ltMPTOKEN_ISSUANCE, issuanceKey}; } +/** @} */ +/** Return the keylet for a holder's MPToken balance entry. + * + * MPToken entries are keyed under the `MPToken` namespace by hashing the + * issuance's 256-bit ledger key together with the holder's `AccountID`. + * This naturally groups all token balances under their issuance in the + * SHAMap hash space. + * + * @param issuanceID 192-bit MPTID identifying the issuance. + * @param holder Account holding the MPToken balance. + * @return Keylet typed `ltMPTOKEN`. + */ +/** @{ */ Keylet mptoken(MPTID const& issuanceID, AccountID const& holder) noexcept; +/** Return a typed keylet for an MPToken entry from its pre-computed key. + * + * @param mptokenKey Pre-computed 256-bit MPToken key. + * @return Keylet typed `ltMPTOKEN`. + */ inline Keylet mptoken(uint256 const& mptokenKey) { return {ltMPTOKEN, mptokenKey}; } +/** Return the keylet for a holder's MPToken balance entry, identified by issuance key. + * + * Use this overload when the issuance's 256-bit ledger key is already available + * to avoid redundant hashing through `makeMptID` and `mptIssuance`. + * + * @param issuanceKey 256-bit key of the `MPTokenIssuance` SLE. + * @param holder Account holding the MPToken balance. + * @return Keylet typed `ltMPTOKEN`. + */ Keylet mptoken(uint256 const& issuanceKey, AccountID const& holder) noexcept; +/** @} */ +/** Return the keylet for a single-asset vault. + * + * @param owner Account that created the vault. + * @param seq Sequence number of the VaultCreate transaction. + * @return Keylet typed `ltVAULT`. + */ +/** @{ */ Keylet vault(AccountID const& owner, std::uint32_t seq) noexcept; +/** Return a typed keylet for a vault from its pre-computed key. + * + * @param vaultKey Pre-computed 256-bit vault key. + * @return Keylet typed `ltVAULT`. + */ inline Keylet vault(uint256 const& vaultKey) { return {ltVAULT, vaultKey}; } +/** @} */ +/** Return the keylet for a loan broker created by an account. + * + * @param owner Account that created the loan broker. + * @param seq Sequence number of the LoanBrokerCreate transaction. + * @return Keylet typed `ltLOAN_BROKER`. + */ +/** @{ */ Keylet loanbroker(AccountID const& owner, std::uint32_t seq) noexcept; +/** Return a typed keylet for a loan broker from its pre-computed key. + * + * @param key Pre-computed 256-bit loan broker key. + * @return Keylet typed `ltLOAN_BROKER`. + */ inline Keylet loanbroker(uint256 const& key) { return {ltLOAN_BROKER, key}; } +/** @} */ +/** Return the keylet for an individual loan issued by a loan broker. + * + * @param loanBrokerID 256-bit key of the parent `LoanBroker` SLE. + * @param loanSeq Sequential loan number assigned by the broker. + * @return Keylet typed `ltLOAN`. + */ +/** @{ */ Keylet loan(uint256 const& loanBrokerID, std::uint32_t loanSeq) noexcept; +/** Return a typed keylet for a loan from its pre-computed key. + * + * @param key Pre-computed 256-bit loan key. + * @return Keylet typed `ltLOAN`. + */ inline Keylet loan(uint256 const& key) { return {ltLOAN, key}; } +/** @} */ +/** Return the keylet for a permissioned domain owned by an account. + * + * @param account Account that created the permissioned domain. + * @param seq Sequence number of the PermissionedDomainSet transaction. + * @return Keylet typed `ltPERMISSIONED_DOMAIN`. + */ +/** @{ */ Keylet permissionedDomain(AccountID const& account, std::uint32_t seq) noexcept; +/** Return the keylet for a permissioned domain from its pre-computed ID. + * + * Use this overload when the domain ID is already known (e.g. stored in + * `sfDomainID` on another SLE) to avoid recomputing the hash. + * + * @param domainID Pre-computed 256-bit domain key. + * @return Keylet typed `ltPERMISSIONED_DOMAIN`. + */ Keylet permissionedDomain(uint256 const& domainID) noexcept; +/** @} */ + } // namespace keylet // Everything below is deprecated and should be removed in favor of keylets: +/** Return the base 256-bit key for an order book directory (deprecated). + * + * The returned key has quality 0 embedded in its last 8 bytes. Prefer + * `keylet::kBOOK(book)` for new code, which wraps this result in a typed keylet. + * + * @param book Order book identifying the in/out asset pair and optional domain. + * @return Raw 256-bit key for the book's root directory. + * @deprecated Use `keylet::kBOOK(book)` instead. + */ uint256 getBookBase(Book const& book); +/** Advance a book-directory key to the next quality level (deprecated). + * + * Adds a unit to the 64-bit quality field embedded in the last 8 bytes of + * @p uBase. Prefer `keylet::kNEXT(k)` for new code. + * + * @param uBase A book-directory key, typically from `getBookBase`. + * @return Key with quality incremented by 1. + * @deprecated Use `keylet::kNEXT(k)` instead. + */ uint256 getQualityNext(uint256 const& uBase); +/** Extract the 64-bit quality value from a book-directory key (deprecated). + * + * Reads the last 8 bytes of @p uBase as a big-endian `uint64_t`, exploiting + * `base_uint`'s big-endian internal layout. + * + * @param uBase A book-directory key produced by `getBookBase` or `keylet::quality`. + * @return The 64-bit quality (inverted exchange rate) embedded in the key. + * @deprecated Callers should use `keylet::quality` for construction instead. + */ // VFALCO This name could be better std::uint64_t getQuality(uint256 const& uBase); +/** Return the 256-bit ledger key for a ticket (deprecated). + * + * @param account Owner of the ticket. + * @param uSequence Sequence number consumed when the ticket was created. + * @return Raw key under the `Ticket` namespace. + * @deprecated Use `keylet::kTICKET(account, seq)` instead. + */ uint256 getTicketIndex(AccountID const& account, std::uint32_t uSequence); +/** Return the 256-bit ledger key for a ticket from a SeqProxy (deprecated). + * + * @param account Owner of the ticket. + * @param ticketSeq SeqProxy in ticket mode. + * @return Raw key under the `Ticket` namespace. + * @deprecated Use `keylet::kTICKET(account, ticketSeq)` instead. + */ uint256 getTicketIndex(AccountID const& account, SeqProxy ticketSeq); +/** Descriptor binding a keylet factory to its expected ledger-entry type name. + * + * Used exclusively by invariant tests (`Invariants_test.cpp`) to enumerate + * keylet functions and verify that the objects they address have the correct + * ledger entry type. Not part of the production ledger-access API. + * + * @tparam KeyletParams Parameter types of the wrapped keylet factory function. + */ template // NOLINTNEXTLINE(cppcoreguidelines-pro-type-member-init) struct KeyletDesc { + /** Keylet factory function for one ledger object type. */ std::function function; + /** Expected `LedgerEntryType` name as a JSON static string, used to + * validate the type of the SLE retrieved at the computed key. */ json::StaticString expectedLEName; + /** Whether to include this keylet in invariant test coverage. */ bool includeInTests{}; }; -// This list should include all of the keylet functions that take a single -// AccountID parameter. +/** All keylet functions that accept a single `AccountID` parameter. + * + * This array drives invariant tests that verify the ledger-entry type of + * the object addressed by each keylet function. When adding a new single- + * `AccountID` keylet, add an entry here so invariant tests automatically + * exercise it. + * + * @note `nftpageMin` is listed even though creating an actual NFT page at + * that key is normally impossible — the invariant checker tests for it + * regardless. + */ std::array, 6> const kDIRECT_ACCOUNT_KEYLETS{ {{.function = &keylet::account, .expectedLEName = jss::AccountRoot, .includeInTests = false}, {.function = &keylet::ownerDir, .expectedLEName = jss::DirectoryNode, .includeInTests = true}, @@ -383,6 +884,17 @@ std::array, 6> const kDIRECT_ACCOUNT_KEYLETS{ {.function = &keylet::nftpageMax, .expectedLEName = jss::NFTokenPage, .includeInTests = true}, {.function = &keylet::did, .expectedLEName = jss::DID, .includeInTests = true}}}; +/** Construct a 192-bit MPT issuance identifier from a sequence number and issuer. + * + * Packs a big-endian 32-bit @p sequence into the first 4 bytes of the `MPTID`, + * followed by the 20-byte @p account. The explicit endian conversion ensures + * canonical byte order for on-ledger storage and byte-by-byte comparison. + * + * @param sequence The issuer's account sequence number at issuance creation + * (stored as `sfSequence` in the `MPTokenIssuance` SLE). + * @param account The issuing account. + * @return 192-bit `MPTID` uniquely addressing this MPT issuance. + */ MPTID makeMptID(std::uint32_t sequence, AccountID const& account); diff --git a/include/xrpl/protocol/InnerObjectFormats.h b/include/xrpl/protocol/InnerObjectFormats.h index 9d07a21d1c..62a0e1626a 100644 --- a/include/xrpl/protocol/InnerObjectFormats.h +++ b/include/xrpl/protocol/InnerObjectFormats.h @@ -4,20 +4,66 @@ namespace xrpl { -/** Manages the list of known inner object formats. +/** Singleton registry of field schemas for all XRPL inner object types. + * + * Inner objects are the structured sub-objects that appear nested inside + * transactions and ledger entries — for example, `sfSigner`, `sfSignerEntry`, + * `sfNFToken`, `sfAuctionSlot`, and `sfPriceData`. This registry plays the + * same role for those nested objects as `TxFormats` plays for top-level + * transactions: it maps each inner object's `SField` code to an `SOTemplate` + * that declares which child fields are `soeREQUIRED`, `soeOPTIONAL`, or + * `soeDEFAULT`. + * + * The key type is `int` (the integer field code returned by + * `SField::getCode()`) rather than a dedicated enum, because inner objects + * are already identified by their `SField` descriptors in wire format. + * + * The registry is complete and immutable after the first call to + * `getInstance()`. Duplicate key registration triggers a `LogicError` at + * static-init time. The returned `const&` from `getInstance()` is safe for + * concurrent reads without additional locking. + * + * @see TxFormats, LedgerFormats, SOTemplate */ class InnerObjectFormats : public KnownFormats { private: - /** Create the object. - This will load the object with all the known inner object formats. - */ + /** Register all known inner object schemas. + * + * Each `add()` call maps an `SField`'s JSON name and integer field code + * to an `SOTemplate` that specifies the required, optional, and default + * child fields. The `SField` code doubles as the registry key, so no + * separate enumeration is needed. + */ InnerObjectFormats(); public: + /** Return the process-wide singleton instance. + * + * Initialized on first call via a Meyer's function-local static; safe + * for concurrent access after construction. The object is immutable + * after it is returned for the first time. + * + * @return A `const` reference to the singleton registry. + */ static InnerObjectFormats const& getInstance(); + /** Look up the field schema for a structured inner object. + * + * Translates an `SField` to its registered `SOTemplate` by matching on + * the field's integer code. The returned pointer is stable for the + * lifetime of the process; callers may cache it safely. + * + * Called by `STObject::makeInnerObject()` (amendment-gated on + * `fixInnerObjTemplate` / `fixInnerObjTemplate2`) and by + * `STObject::applyTemplateFromSField()` to enforce field-presence rules + * during construction and deserialization. + * + * @param sField The `SField` identifying the inner object type. + * @return A pointer to the matching `SOTemplate`, or `nullptr` if + * `sField` is not a registered inner object type. + */ [[nodiscard]] SOTemplate const* findSOTemplateBySField(SField const& sField) const; }; diff --git a/include/xrpl/protocol/Issue.h b/include/xrpl/protocol/Issue.h index 3b74b9132a..d8dc161568 100644 --- a/include/xrpl/protocol/Issue.h +++ b/include/xrpl/protocol/Issue.h @@ -6,36 +6,113 @@ namespace xrpl { -/** A currency issued by an account. - @see Currency, AccountID, Issue, Book -*/ +/** Identifies a specific currency as issued by a specific account. + * + * `Issue` is the minimal token identity tuple in the XRPL type system: a + * 160-bit `Currency` paired with a 160-bit `AccountID`. It is the building + * block for trust lines, order books, offer matching, and AMM pools. + * + * XRP is represented as a special-case `Issue` whose `currency` and + * `account` fields both carry their respective zero/sentinel values + * (`xrpCurrency()` and `xrpAccount()`). The equality and ordering + * operators ignore `account` when `currency` is XRP, so all XRP issues + * form a single equivalence class even if `account` carries a stale value. + * + * @note `Issue` is a peer to `MPTIssue`; both satisfy the `IssueType` + * concept in `Concepts.h` and can be held inside an `Asset` variant. + * @see MPTIssue, Asset, Book + */ class Issue { public: + /** The 160-bit currency code. `xrpCurrency()` (all-zero) denotes XRP. */ Currency currency; + + /** The account that issued this currency. + * + * Meaningful only for non-XRP issues. For XRP issues this field + * should carry `xrpAccount()` (all-zero); `isConsistent()` enforces + * that invariant, but the comparison operators are lenient and ignore + * this field when `currency` is XRP. + */ AccountID account; Issue() = default; + /** Constructs an issue from an explicit currency and issuer account. + * + * @param c The currency code. + * @param a The issuing account. Pass `xrpAccount()` when `c` is + * `xrpCurrency()`; use `isConsistent()` to verify the pair. + */ Issue(Currency const& c, AccountID const& a) : currency(c), account(a) { } + /** Returns the issuing account. + * + * Provides a uniform accessor shared with `MPTIssue`, enabling generic + * algorithms to retrieve the issuer without a type dispatch. For XRP + * issues the returned value is `xrpAccount()` (all-zero). + * + * @return A reference to `account`. + */ [[nodiscard]] AccountID const& getIssuer() const { return account; } + /** Returns a human-readable diagnostic string of the form + * `currency[/account]`. + * + * For XRP, only the currency string is returned. For IOU issues the + * account is appended after a slash, substituting `"0"` for + * `xrpAccount()` and `"1"` for `noAccount()` so structurally + * inconsistent issues are detectable in logs. + * + * @note Field order is `currency/account`, which is the reverse of + * `to_string(Issue)`. Use this for logging; use `setJson()` for + * canonical wire output. + * @return Diagnostic string; never empty. + */ [[nodiscard]] std::string getText() const; + /** Writes the canonical JSON representation into an existing object. + * + * Always sets `jv["currency"]`. Sets `jv["issuer"]` as a Base58Check + * account string only for non-XRP issues; XRP omits `"issuer"` + * entirely, which is the authoritative form expected by transaction + * JSON, RPC responses, and the binary codec. + * + * @param jv Output JSON object; existing keys are not cleared. + */ void setJson(json::Value& jv) const; + /** Returns `true` if this issue represents XRP, the native asset. + * + * Implemented as a full equality comparison against `xrpIssue()`. + * The underlying `operator==` short-circuits on `currency` alone for + * XRP, so `account` is not consulted. + * + * @return `true` iff `*this == xrpIssue()`. + */ [[nodiscard]] bool native() const; + /** Returns `true` if amounts of this issue are stored as integers. + * + * For `Issue`, only XRP uses integer (drop) representation; all IOU + * currencies use mantissa/exponent floating-point. Delegates entirely + * to `native()`. + * + * @note `MPTIssue::integral()` always returns `true`. The shared + * method name allows generic code to query integer-vs-float + * semantics without a type dispatch. + * @return `true` iff `native()`. + */ [[nodiscard]] bool integral() const; @@ -43,21 +120,92 @@ public: operator<=>(Issue const& lhs, Issue const& rhs); }; +/** Returns `true` if `ac.currency` and `ac.account` agree on XRP-ness. + * + * A well-formed XRP issue must carry `xrpCurrency()` and `xrpAccount()` + * (both all-zero). A well-formed IOU issue must carry a non-zero currency + * and a non-zero account. Cross-contamination — XRP currency with a real + * account, or a real currency with the XRP account sentinel — silently + * corrupts amount comparisons and offer-book matching. + * + * @note The equality and ordering operators are intentionally more lenient + * than this check; they ignore `account` whenever `currency` is XRP. + * Call `isConsistent()` on any `Issue` sourced from external input. + * @param ac The issue to validate. + * @return `true` iff `isXRP(ac.currency) == isXRP(ac.account)`. + */ bool isConsistent(Issue const& ac); +/** Returns a string of the form `account/currency`, or just the currency + * for XRP issues. + * + * @note Field order is `account/currency`, which is the reverse of + * `Issue::getText()` (`currency/account`). Both formats are in active + * use in different parts of the codebase; this one matches + * offer-book log lines and stream output. + * @param ac The issue to render. + * @return A non-empty string identifying the issue. + */ std::string to_string(Issue const& ac); +/** Returns the canonical wire-format JSON representation of an issue. + * + * Convenience wrapper around `Issue::setJson()`. The returned object + * contains a `"currency"` field and, for non-XRP issues, an `"issuer"` + * field holding the Base58Check-encoded account. + * + * @param is The issue to serialise. + * @return A new JSON object representing the issue. + */ json::Value toJson(Issue const& is); +/** Parses and validates an `Issue` from a JSON object. + * + * Performs layered validation in strict order: + * 1. `v` must be a JSON object. + * 2. `mpt_issuance_id` must be absent — its presence means the caller + * has accidentally routed MPT data into the wrong parser. + * 3. `"currency"` must be a string that parses to neither `badCurrency()` + * nor `noCurrency()`. + * 4. For XRP currency, `"issuer"` must be absent. + * 5. For non-XRP currencies, `"issuer"` must be a valid Base58Check + * account string. + * + * @param v The JSON value to parse; must be a JSON object. + * @return The parsed `Issue`. + * @throws std::runtime_error if `v` is not an object, or if + * `mpt_issuance_id` is present. + * @throws Json::error if any field is missing, the wrong type, or carries + * an invalid currency code or account string. + * @see toJson for the inverse operation. + */ Issue issueFromJson(json::Value const& v); +/** Writes the issue to a stream using the `to_string(Issue)` format. + * + * @param os The output stream. + * @param x The issue to write. + * @return `os`. + */ std::ostream& operator<<(std::ostream& os, Issue const& x); +/** Appends both `currency` and `account` to the hasher unconditionally. + * + * @note The XRP special case from `operator==` (ignoring `account` when + * `currency` is XRP) is deliberately not applied here. Consistent + * data — ensured by `isConsistent()` at ingestion — guarantees that + * XRP issues always carry `xrpAccount()`, so hashes are stable. + * Hashing an inconsistent XRP issue could produce a hash that matches + * equality but diverges from a canonical XRP issue's hash. + * @tparam Hasher A type satisfying the `beast::hash_append` concept. + * @param h The hasher to append to. + * @param r The issue whose fields are appended. + */ template void hash_append(Hasher& h, Issue const& r) @@ -66,17 +214,35 @@ hash_append(Hasher& h, Issue const& r) hash_append(h, r.currency, r.account); } -/** Equality comparison. */ -/** @{ */ +/** Returns `true` if two issues represent the same asset. + * + * Currencies are compared first. When both currencies are XRP (all-zero), + * the `account` field is ignored — all XRP issues are equal regardless of + * any stale or partially-constructed account value. For IOU issues both + * fields must match exactly. + * + * @param lhs Left-hand issue. + * @param rhs Right-hand issue. + * @return `true` iff the two issues identify the same asset. + */ [[nodiscard]] constexpr bool operator==(Issue const& lhs, Issue const& rhs) { return (lhs.currency == rhs.currency) && (isXRP(lhs.currency) || lhs.account == rhs.account); } -/** @} */ -/** Strict weak ordering. */ -/** @{ */ +/** Provides a strict weak ordering over `Issue` values. + * + * Sorts by `currency` first. When currencies are equal and the currency is + * XRP, `std::weak_ordering::equivalent` is returned immediately so that all + * XRP issues form a single equivalence class regardless of the `account` + * field. For IOU issues with equal currencies, `account` is the + * tiebreaker. + * + * @param lhs Left-hand issue. + * @param rhs Right-hand issue. + * @return A `std::weak_ordering` value consistent with `operator==`. + */ [[nodiscard]] constexpr std::weak_ordering operator<=>(Issue const& lhs, Issue const& rhs) { @@ -88,11 +254,18 @@ operator<=>(Issue const& lhs, Issue const& rhs) return (lhs.account <=> rhs.account); } -/** @} */ //------------------------------------------------------------------------------ -/** Returns an asset specifier that represents XRP. */ +/** Returns the canonical `Issue` sentinel that represents XRP. + * + * The returned instance holds `xrpCurrency()` and `xrpAccount()` (both + * all-zero 160-bit values). The singleton is initialised once and returned + * by `const&`, which is thread-safe under C++11 guaranteed-initialisation + * semantics. + * + * @return A reference to the process-lifetime XRP issue singleton. + */ inline Issue const& xrpIssue() { @@ -100,7 +273,13 @@ xrpIssue() return kISSUE; } -/** Returns an asset specifier that represents no account and currency. */ +/** Returns an `Issue` sentinel that represents the absence of an issue. + * + * Holds `noCurrency()` and `noAccount()`. Used in contexts where a + * missing or invalid issue must be represented without `std::optional`. + * + * @return A reference to the process-lifetime "no issue" singleton. + */ inline Issue const& noIssue() { @@ -108,6 +287,14 @@ noIssue() return kISSUE; } +/** Returns `true` if `issue` represents XRP, the native asset. + * + * Thin wrapper over `issue.native()`, providing the naming convention + * used throughout the codebase for XRP detection at all abstraction levels. + * + * @param issue The issue to test. + * @return `true` iff `issue.native()`. + */ inline bool isXRP(Issue const& issue) { diff --git a/include/xrpl/protocol/KeyType.h b/include/xrpl/protocol/KeyType.h index c709eb897a..041e28f077 100644 --- a/include/xrpl/protocol/KeyType.h +++ b/include/xrpl/protocol/KeyType.h @@ -1,3 +1,12 @@ +/** @file + * Discriminant enum and conversion utilities for XRPL's two signature schemes. + * + * Every key-management function in the protocol (generation, signing, + * verification) accepts a `KeyType` to select between the secp256k1 and + * ed25519 algorithms. This header is included by virtually every + * cryptographic interface in the protocol layer. + */ + #pragma once #include @@ -5,11 +14,38 @@ namespace xrpl { +/** Selects the cryptographic signature algorithm for a key pair. + * + * The XRPL supports two independent signature schemes: the Bitcoin-lineage + * secp256k1 elliptic curve and the modern ed25519 Edwards curve. The + * explicit integer values provide stable identifiers for internal storage and + * configuration that maps an integer to a key type, even though `KeyType` + * itself is not serialized directly on the wire. + * + * The choice of algorithm is self-describing in serialized key material: + * secp256k1 public keys begin with a compressed-point prefix byte (`0x02` or + * `0x03`), while ed25519 public keys carry a sentinel byte `0xED`. + * + * @see publicKeyType for recovering the algorithm from a serialized public key. + */ enum class KeyType { - Secp256k1 = 0, - Ed25519 = 1, + Secp256k1 = 0, /**< Bitcoin-lineage elliptic curve; XRPL uses a custom + seed-to-key-pair derivation path. */ + Ed25519 = 1, /**< Modern Edwards curve; uses a direct derivation from the + seed with no intermediate generator step. */ }; +/** Parse a canonical string name into a `KeyType`. + * + * Recognises exactly `"secp256k1"` and `"ed25519"` (lower-case). Returns an + * empty optional for any other input rather than throwing, so callers at + * configuration-parse or RPC-request boundaries can compose the result with + * their own error-reporting logic without requiring exception handling. + * + * @param s The string to parse. + * @return The corresponding `KeyType`, or `std::nullopt` if `s` is not a + * recognised algorithm name. + */ inline std::optional keyTypeFromString(std::string const& s) { @@ -22,6 +58,17 @@ keyTypeFromString(std::string const& s) return {}; } +/** Return the canonical lower-case string name for a `KeyType`. + * + * Returns `"INVALID"` — rather than `nullptr` or undefined behaviour — for + * any value that matches neither known enumerator. This defensive path is + * reachable because C++ permits arbitrary integers to be cast to an + * `enum class`, so a corrupt or adversarially crafted value must not cause + * unsafe access in logging or diagnostic paths. + * + * @param type The key type to convert. + * @return `"secp256k1"`, `"ed25519"`, or `"INVALID"`. + */ inline char const* to_string(KeyType type) { @@ -34,6 +81,18 @@ to_string(KeyType type) return "INVALID"; } +/** Write a `KeyType` to a stream as its canonical string name. + * + * Templated on `Stream` rather than fixed to `std::ostream` so the operator + * works with Beast logging streams, test-harness formatters, and any other + * stream-like type without coupling this header to a concrete stream + * hierarchy. + * + * @tparam Stream Any type that supports `operator<<(char const*)`. + * @param s The destination stream. + * @param type The key type to write. + * @return `s`, to allow chaining. + */ template inline Stream& operator<<(Stream& s, KeyType type) diff --git a/include/xrpl/protocol/Keylet.h b/include/xrpl/protocol/Keylet.h index 19704e2a11..085136dcbf 100644 --- a/include/xrpl/protocol/Keylet.h +++ b/include/xrpl/protocol/Keylet.h @@ -7,24 +7,72 @@ namespace xrpl { class STLedgerEntry; -/** A pair of SHAMap key and LedgerEntryType. - - A Keylet identifies both a key in the state map - and its ledger entry type. - - @note Keylet is a portmanteau of the words key - and LET, an acronym for LedgerEntryType. -*/ +/** Bundles the 256-bit SHAMap locator of a ledger object with its expected + * `LedgerEntryType`, making ledger lookups type-safe by construction. + * + * The name is a portmanteau of "key" and "LET" (LedgerEntryType). Callers + * never build a `Keylet` by hand — they use one of the factory functions in + * the `keylet::` namespace (see `Indexes.h`), each of which encapsulates the + * correct SHA-512Half derivation for a specific object type and returns a + * `Keylet` already annotated with the matching `LedgerEntryType`. + * + * The ledger view's `read()` method accepts a `Keylet` and calls `check()` + * before returning an entry, so an incorrect type annotation surfaces at + * the access point rather than silently yielding a mistyped object. + * + * Two sentinel types participate in the matching protocol but are never + * stored on-ledger: + * - `ltANY` — wildcard; bypasses type checking entirely (used by + * `keylet::unchecked`). + * - `ltCHILD` — matches any entry that is not a directory node, reflecting + * the semantics of owner-directory children. + * + * @see keylet namespace in `Indexes.h` for all factory functions. + */ struct Keylet { + /** 256-bit SHAMap key that locates the ledger entry in the state tree. */ uint256 key; + + /** Expected `LedgerEntryType` of the entry at `key`. + * + * May be the sentinel `ltANY` (wildcard) or `ltCHILD` (any non-directory + * entry); all other values are concrete on-ledger types whose numeric + * identities are protocol-stable and consensus-critical. + */ LedgerEntryType type; + /** Constructs a keylet from an explicit type and key. + * + * Prefer the factory functions in the `keylet::` namespace over calling + * this constructor directly; they ensure the correct key derivation + * formula is used for each ledger object type. + * + * @param type The expected `LedgerEntryType` of the addressed entry. + * @param key The 256-bit SHAMap key of the entry. + */ Keylet(LedgerEntryType type, uint256 const& key) : key(key), type(type) { } - /** Returns true if the SLE matches the type */ + /** Validates that a deserialized ledger entry corresponds to this keylet. + * + * Applies a three-tier match ordered from most-permissive to most-strict: + * - `ltANY`: always returns `true`; the caller bears full responsibility + * for type safety. + * - `ltCHILD`: returns `true` for any entry whose concrete type is not + * `ltDIR_NODE`. Directory nodes are structural bookkeeping objects; a + * directory child is definitionally something other than a directory. + * - Concrete type: requires both `sle.getType() == type` and + * `sle.key() == key`. This is the common case and gives the strongest + * safety guarantee. + * + * @param sle The deserialized ledger entry retrieved from the state map. + * @return `true` if `sle` legitimately corresponds to this keylet. + * @note `sle` must not itself carry `ltANY` or `ltCHILD` as its stored + * type; those are query-side sentinels, not real on-ledger types. + * An `XRPL_ASSERT` enforces this precondition in debug builds. + */ [[nodiscard]] bool check(STLedgerEntry const&) const; }; diff --git a/include/xrpl/protocol/KnownFormats.h b/include/xrpl/protocol/KnownFormats.h index 9aa914ba97..323f16cb08 100644 --- a/include/xrpl/protocol/KnownFormats.h +++ b/include/xrpl/protocol/KnownFormats.h @@ -1,3 +1,13 @@ +/** + * @file KnownFormats.h + * @brief Template base for XRPL protocol format registries. + * + * Declares `KnownFormats`, the shared infrastructure used + * by `TxFormats`, `LedgerFormats`, and `InnerObjectFormats` to register and + * look up the field schemas (`SOTemplate`) for every transaction type, ledger + * object type, and inner object type in the protocol. + */ + #pragma once #include @@ -11,22 +21,60 @@ namespace xrpl { -/** Manages a list of known formats. - - Each format has a name, an associated KeyType (typically an enumeration), - and a predefined @ref SOElement. - - @tparam KeyType The type of key identifying the format. -*/ +/** Registry of protocol format schemas, keyed by a wire-protocol discriminant. + * + * Each concrete registry (transaction, ledger entry, inner object) inherits + * from this template and populates it with one `Item` per recognized format. + * At runtime the serialization and validation layers look up `Item` instances + * via `findByType()` or `findTypeByName()` to obtain the `SOTemplate` that + * governs which fields are required, optional, or default for that format. + * + * Concrete subclasses are singletons constructed during static initialization; + * registering the same `KeyType` value twice is a programming error caught via + * `logicError()` (process abort) at startup rather than at request time. + * + * @note `begin()` / `end()` expose the raw `forward_list` for use in tests. + * They are not part of the normal lookup API. + * + * @tparam KeyType Integral or enum type whose values are the wire-protocol + * discriminants for this family of formats (e.g. `TxType`, + * `LedgerEntryType`, or `int` for inner objects). The `static_assert` + * inside `Item`'s constructor enforces this constraint at compile time. + * @tparam Derived The concrete subclass (CRTP). Used solely to embed the + * subclass name in diagnostic messages via `beast::typeName()`. + * + * @see TxFormats, LedgerFormats, InnerObjectFormats, SOTemplate + */ template class KnownFormats { public: - /** A known format. + /** A registered protocol format: name, wire-key, and field schema. + * + * Each `Item` bundles the human-readable format name (e.g. `"Payment"`), + * its `KeyType` discriminant (the integer embedded in the wire protocol), + * and the `SOTemplate` that specifies every field's presence requirement. + * `Item` instances are owned by `KnownFormats` and are never moved after + * construction; callers may hold `Item const*` pointers indefinitely. */ class Item { public: + /** Construct a format item and merge unique and common field lists. + * + * `uniqueFields` are specific to this format; `commonFields` are + * shared across all formats in the registry (e.g. ledger metadata + * fields). Both lists are forwarded into the `SOTemplate`. + * + * @note A `static_assert` enforces at compile time that `KeyType` is + * integral or an enum, preventing accidental use of arbitrary + * types as wire-protocol discriminants. + * + * @param name Human-readable format name (e.g. `"Payment"`). + * @param type Wire-protocol discriminant value. + * @param uniqueFields Fields specific to this format. + * @param commonFields Fields shared by all formats in this registry. + */ Item( char const* name, KeyType type, @@ -36,13 +84,13 @@ public: , name_(name) , type_(type) { - // Verify that KeyType is appropriate. + // KeyType must map directly to a wire integer value. static_assert( std::is_enum_v || std::is_integral_v, "KnownFormats KeyType must be integral or enum."); } - /** Retrieve the name of the format. + /** Return the human-readable name of this format (e.g. `"Payment"`). */ [[nodiscard]] std::string const& getName() const @@ -50,7 +98,11 @@ public: return name_; } - /** Retrieve the transaction type this format represents. + /** Return the wire-protocol discriminant identifying this format. + * + * The returned value is the `KeyType` constant that was supplied at + * registration time — for example `ttPayment` for a transaction + * format, or `ltOFFER` for a ledger entry format. */ [[nodiscard]] KeyType getType() const @@ -58,6 +110,13 @@ public: return type_; } + /** Return the field schema for this format. + * + * The `SOTemplate` enumerates every field that may appear in a + * serialized object of this type, together with its `SOEStyle` + * (`soeREQUIRED`, `soeOPTIONAL`, or `soeDEFAULT`). The returned + * reference is stable for the lifetime of the process. + */ [[nodiscard]] SOTemplate const& getSOTemplate() const { @@ -70,32 +129,40 @@ public: KeyType const type_; }; - /** Create the known formats object. - - Derived classes will load the object with all the known formats. - */ private: + /** Construct the registry and capture the concrete subclass name. + * + * The subclass name (obtained via `beast::typeName()`) is + * stored in `name_` and prepended to diagnostic messages emitted by + * `findTypeByName()`, enabling errors like + * `"TxFormats: Unknown format name 'BadName'"`. + * + * Only `Derived` may construct this base (enforced by the `friend` + * declaration and the private access specifier). + */ KnownFormats() : name_(beast::typeName()) { } public: - /** Destroy the known formats object. - - The defined formats are deleted. - */ virtual ~KnownFormats() = default; KnownFormats(KnownFormats const&) = delete; KnownFormats& operator=(KnownFormats const&) = delete; - /** Retrieve the type for a format specified by name. - - If the format name is unknown, an exception is thrown. - - @param name The name of the type. - @return The type. - */ + /** Return the wire-protocol key for a format looked up by name. + * + * Intended for use when parsing externally supplied strings (e.g. JSON + * RPC input or configuration files). An unknown name is treated as a + * recoverable application-level error and is reported as a + * `std::runtime_error` whose message includes the registry name and the + * (possibly truncated to 32 characters) unrecognized name. + * + * @param name The human-readable format name to look up. + * @return The `KeyType` value registered under that name. + * @throws std::runtime_error If `name` is not registered in this + * registry. + */ [[nodiscard]] KeyType findTypeByName(std::string const& name) const { @@ -106,7 +173,16 @@ public: name.substr(0, std::min(name.size(), std::size_t(32))) + "'"); } - /** Retrieve a format based on its type. + /** Return the `Item` registered for the given wire-protocol key, or + * `nullptr` if no such format has been registered. + * + * Returns `nullptr` on a miss so that callers in internal code paths can + * handle an absent format with an idiomatic null check rather than a + * caught exception. Contrast with `findTypeByName()`, which throws for + * unknown names supplied from external input. + * + * @param type The wire-protocol discriminant to look up. + * @return Pointer to the matching `Item`, or `nullptr`. */ [[nodiscard]] Item const* findByType(KeyType type) const @@ -117,13 +193,21 @@ public: return itr->second; } - // begin() and end() are provided for testing purposes. + /** Return an iterator to the first registered `Item`. + * + * @note Exposed for testing only; do not rely on iteration order, which + * reflects reverse-registration sequence due to `emplace_front`. + */ [[nodiscard]] typename std::forward_list::const_iterator begin() const { return formats_.begin(); } + /** Return a past-the-end iterator for the registered `Item` sequence. + * + * @note Exposed for testing only. + */ [[nodiscard]] typename std::forward_list::const_iterator end() const { @@ -131,7 +215,14 @@ public: } protected: - /** Retrieve a format based on its name. + /** Return the `Item` registered under the given name, or `nullptr`. + * + * Protected so that external callers are directed to the public + * `findTypeByName()`, which enforces the exception-on-miss contract for + * externally supplied names. + * + * @param name The human-readable format name to look up. + * @return Pointer to the matching `Item`, or `nullptr`. */ [[nodiscard]] Item const* findByName(std::string const& name) const @@ -142,15 +233,28 @@ protected: return itr->second; } - /** Add a new format. - - @param name The name of this format. - @param type The type of this format. - @param uniqueFields A std::vector of unique fields - @param commonFields A std::vector of common fields - - @return The created format. - */ + /** Register a new format with this registry. + * + * Creates an `Item` by combining `uniqueFields` (specific to this + * format) and `commonFields` (shared across all formats in the registry) + * into a single `SOTemplate`. The new `Item` is inserted at the front + * of the owning `forward_list` to preserve pointer stability, then + * indexed by both name and type in the two `flat_map` lookup tables. + * + * Registering a `type` value that is already present is a programming + * error: `logicError()` (process abort) is called immediately, making + * the failure visible at static-initialization time before any requests + * are served. + * + * @param name Human-readable format name (e.g. `"Payment"`). + * @param type Wire-protocol discriminant; must be unique within + * this registry. + * @param uniqueFields Fields specific to this format. + * @param commonFields Fields shared by all formats in this registry; + * defaults to empty. + * @return A stable `const` reference to the newly created + * `Item`. + */ Item const& add(char const* name, KeyType type, @@ -174,14 +278,22 @@ protected: } private: + /** Concrete subclass name, captured at construction for diagnostic messages. */ std::string name_; - // One of the situations where a std::forward_list is useful. We want to - // store each Item in a place where its address won't change. So a node- - // based container is appropriate. But we don't need searchability. + /** Owning store for all registered `Item` instances. + * + * `std::forward_list` is used because node-based containers never + * relocate existing elements, keeping `Item` addresses stable after + * insertion. The `flat_map` indices below store raw pointers into this + * list; pointer stability is therefore a hard requirement. + */ std::forward_list formats_{}; + /** Name-to-item index for O(log n) lookup by human-readable format name. */ boost::container::flat_map names_{}; + + /** Type-to-item index for O(log n) lookup by wire-protocol discriminant. */ boost::container::flat_map types_{}; friend Derived; }; diff --git a/include/xrpl/protocol/LedgerFormats.h b/include/xrpl/protocol/LedgerFormats.h index 99d5d818f1..0e0712dd9a 100644 --- a/include/xrpl/protocol/LedgerFormats.h +++ b/include/xrpl/protocol/LedgerFormats.h @@ -1,3 +1,27 @@ +/** @file + * Authoritative registry for every object type that can live in the XRP Ledger. + * + * Defines three tightly-coupled, protocol-level artifacts: + * + * 1. `LedgerEntryType` — the `uint16_t` wire discriminants stored inside every + * serialized ledger object. + * 2. `LedgerSpecificFlags` / per-object flag accessor functions / `getAllLedgerFlags()` + * — flag bitmasks that modify ledger object behavior, together with Meyer's-singleton + * accessors consumed by the `server_definitions` RPC endpoint. + * 3. `LedgerFormats` — the singleton registry that maps each `LedgerEntryType` to its + * `SOTemplate` (field presence schema). + * + * The `ledger_entries.macro` X-macro file is the single source of truth for all + * per-type data; this header and `LedgerFormats.cpp` each include it with a different + * macro definition to derive the enum and the format registration from the same table. + * + * @warning All numeric values defined here are embedded in serialized ledger objects + * and transmitted over the wire. Changing them without corresponding amendment + * machinery causes a hard fork. + * + * @ingroup protocol + */ + #pragma once // NOLINTBEGIN(readability-identifier-naming) @@ -9,28 +33,35 @@ #include namespace xrpl { -/** Identifiers for on-ledger objects. - Each ledger object requires a unique type identifier, which is stored within the object itself; - this makes it possible to iterate the entire ledger and determine each object's type and verify - that the object you retrieved from a given hash matches the expected type. - - @warning Since these values are stored inside objects stored on the ledger they are part of the - protocol. - **Changing them should be avoided because without special handling, this will result in a hard - fork.** - - @note Values outside this range may be used internally by the code for various purposes, but - attempting to use such values to identify on-ledger objects will result in an invariant failure. - - @note When retiring types, the specific values should not be removed but should be marked as - [[deprecated]]. This is to avoid accidental reuse of identifiers. - - @todo The C++ language does not enable checking for duplicate values here. - If it becomes possible then we should do this. - - @ingroup protocol -*/ +/** Numeric type identifiers for every object type that can exist in the XRP Ledger. + * + * Each ledger object embeds its `LedgerEntryType` in the serialized form; this allows + * the ledger layer to determine an object's type during iteration and to verify that + * a hash lookup returned the expected kind of object. + * + * The concrete values are generated by the `ledger_entries.macro` X-macro, which is + * the single source of truth for (tag, value, name, fields) tuples across the enum, + * the `LedgerFormats` constructor, and any auto-generated protocol bindings. + * + * Beyond the macro-generated members, two sentinel pseudo-types (`ltANY`, `ltCHILD`) + * are defined manually for use in keylet lookups where the precise object type is + * unknown or irrelevant. + * + * @warning These values are stored in serialized ledger objects and are part of the + * protocol. Changing them without special amendment handling causes a hard fork. + * + * @note Values outside the known range may be used internally, but passing them to + * ledger-object APIs will result in an invariant failure. + * + * @note When retiring an entry type, mark its enumerator `[[deprecated]]` rather than + * removing it. Removal would free the numeric slot for accidental reuse. + * + * @todo C++ enums cannot enforce uniqueness of values at compile time; duplicate IDs + * can silently coexist. If the language gains that capability it should be used here. + * + * @ingroup protocol + */ // Protocol-critical, hundreds of usages // NOLINTNEXTLINE(cppcoreguidelines-use-enum-class) enum LedgerEntryType : std::uint16_t { @@ -96,16 +127,29 @@ enum LedgerEntryType : std::uint16_t { 0x0067, }; -/** Ledger object flags. - - These flags are specified in ledger objects and modify their behavior. - - @warning Ledger object flags form part of the protocol. - **Changing them should be avoided because without special handling, this will result in a hard - fork.** - - @ingroup protocol -*/ +/** Flat enum of all per-object flag bitmasks across every ledger entry type. + * + * Each enumerator is a named bit constant (e.g. `lsfRequireDestTag`, `lsfGlobalFreeze`) + * that modifies the behavior of a specific ledger object type. The constants are + * generated via the `XMACRO` / `TO_VALUE` pass below, which strips object-type grouping + * and collects every flag name and value into a single flat enum. + * + * @note `LSF_FLAG2` is used when the same bit value appears in more than one object + * type (currently `lsfMPTLocked = 0x00000001` shared between `MPTokenIssuance` and + * `MPToken`). The second occurrence is silently omitted from this enum via the + * `NULL_OUTPUT` helper to avoid a duplicate-enumerator warning, while still appearing + * in the per-object flag maps returned by the getter functions below. + * + * @note Most object types use flag bits starting at `0x00010000`, reserving the low 16 + * bits for future use. `DirNode`, `NFTokenOffer`, and the MPToken family deviate + * from this convention and use the low-order bits — a legacy of their original + * feature designs. + * + * @warning These values are stored in serialized ledger objects and form part of the + * protocol. Changing them without amendment machinery causes a hard fork. + * + * @ingroup protocol + */ #pragma push_macro("XMACRO") #pragma push_macro("TO_VALUE") #pragma push_macro("VALUE_TO_MAP") @@ -222,19 +266,17 @@ enum LedgerEntryType : std::uint16_t { // NOLINTNEXTLINE(cppcoreguidelines-use-enum-class) enum LedgerSpecificFlags : std::uint32_t { XMACRO(NULL_NAME, TO_VALUE, NULL_OUTPUT) }; -// Create getter functions for each set of flags using Meyer's singleton pattern. -// This avoids static initialization order fiasco while still providing efficient access. -// This is used below in `getAllLedgerFlags()` to generate the server_definitions RPC output. -// -// example: -// inline LedgerFlagMap const& getAccountRootFlags() { -// static LedgerFlagMap const flags = { -// {"lsfPasswordSpent", 0x00010000}, -// {"lsfRequireDestTag", 0x00020000}, -// ...}; -// return flags; -// } +/** Map from flag name string to its `uint32_t` bitmask value for a single ledger object type. + * + * Each entry has the form `{"lsfFlagName", 0xXXXXXXXX}`. Per-object maps are returned + * by the `getFlags()` inline functions generated below and are aggregated by + * `getAllLedgerFlags()` for the `server_definitions` RPC response. + */ using LedgerFlagMap = std::map; + +// Per-object flag getter functions: getAccountRootFlags(), getOfferFlags(), etc. +// Each returns a const LedgerFlagMap& initialized once via Meyer's singleton. +// See getAllLedgerFlags() for the aggregating accessor. #define VALUE_TO_MAP(name, value) {#name, value}, #define TO_MAP(name, values) \ inline LedgerFlagMap const& get##name##Flags() \ @@ -244,16 +286,17 @@ using LedgerFlagMap = std::map; } XMACRO(TO_MAP, VALUE_TO_MAP, VALUE_TO_MAP) -// Create a getter function for all ledger flag maps using Meyer's singleton pattern. -// This is used to generate the server_definitions RPC output. -// -// example: -// inline std::vector> const& getAllLedgerFlags() { -// static std::vector> const flags = { -// {"AccountRoot", getAccountRootFlags()}, -// ...}; -// return flags; -// } +/** Return the flags for all ledger object types, keyed by object type name. + * + * Aggregates every per-object `LedgerFlagMap` (produced by the `getFlags()` + * inline functions above) into a single vector, where each element is a pair of + * `(object-type-name, flag-map)`. The vector is initialized once via Meyer's singleton. + * + * This function is the sole data source for the `server_definitions` RPC endpoint, + * which exposes the complete ledger flag catalogue to external API consumers. + * + * @return A stable `const` reference to the process-wide flag catalogue. + */ #define ALL_LEDGER_FLAGS(name, values) {#name, get##name##Flags()}, inline std::vector> const& getAllLedgerFlags() @@ -280,21 +323,64 @@ getAllLedgerFlags() //------------------------------------------------------------------------------ -/** Holds the list of known ledger entry formats. +/** Singleton registry mapping every `LedgerEntryType` to its canonical field schema. + * + * Inherits from `KnownFormats` (CRTP), which provides + * O(log n) lookup by type and by name, duplicate-registration detection, and stable + * `Item` pointer identity. + * + * The registry is populated once, during static initialization, via the private + * constructor's X-macro pass over `ledger_entries.macro`. Every registered entry + * receives an `SOTemplate` built from its type-specific fields plus the three + * common fields returned by `getCommonFields()` (`sfLedgerIndex`, `sfLedgerEntryType`, + * `sfFlags`). + * + * Callers in the serialization, deserialization, and invariant-checking layers + * access the registry through `getInstance()` to look up schemas by type. + * + * @see KnownFormats, LedgerEntryType, SOTemplate + * @ingroup protocol */ class LedgerFormats : public KnownFormats { private: - /** Create the object. - This will load the object with all the known ledger formats. - */ + /** Populate the registry with all known ledger entry formats. + * + * Uses an X-macro pass over `ledger_entries.macro`, registering each entry type + * by calling `KnownFormats::add()` with the entry's name, `LedgerEntryType` + * discriminant, type-specific fields, and the common fields from `getCommonFields()`. + * + * If `ledger_entries.macro` contains a duplicate numeric type ID, `add()` calls + * `logicError()` (process abort) during static initialization rather than silently + * corrupting the registry. + */ LedgerFormats(); public: + /** Return the process-wide `LedgerFormats` singleton. + * + * Uses a function-local static (Meyer's singleton) for thread-safe, once-only + * initialization guaranteed by C++11. The first call constructs the registry and + * registers every known ledger entry type; subsequent calls return the same instance. + * + * @return A `const` reference to the global `LedgerFormats` instance. + */ static LedgerFormats const& getInstance(); - // Fields shared by all ledger entry formats: + /** Return the three fields that every ledger entry must carry. + * + * The common fields are: + * - `sfLedgerIndex` (`soeOPTIONAL`) — key of the entry in the SHAMap. + * - `sfLedgerEntryType` (`soeREQUIRED`) — wire discriminant; must always be present. + * - `sfFlags` (`soeREQUIRED`) — object flag bitmask; must always be present. + * + * These fields are injected into every `SOTemplate` by the constructor, so they do + * not need to be listed in each entry type's individual field set in `ledger_entries.macro`. + * The vector is initialized once on first call (function-local static). + * + * @return A stable `const` reference to the common-fields vector. + */ static std::vector const& getCommonFields(); }; diff --git a/include/xrpl/protocol/LedgerHeader.h b/include/xrpl/protocol/LedgerHeader.h index 68dd5e24b9..d67a89a94a 100644 --- a/include/xrpl/protocol/LedgerHeader.h +++ b/include/xrpl/protocol/LedgerHeader.h @@ -1,3 +1,13 @@ +/** @file + * Defines `LedgerHeader`, the compact canonical summary of a single XRP + * Ledger, together with the serialization, deserialization, and hash + * calculation functions that operate on it. + * + * Every ledger — open, closed, or validated — is identified and + * authenticated through this structure. The serialized form is + * protocol-immutable: 118 bytes without the trailing hash, 150 with it. + */ + #pragma once #include @@ -9,7 +19,23 @@ namespace xrpl { -/** Information about the notional ledger backing the view. */ +/** Canonical metadata block that identifies a single XRP Ledger. + * + * Fields are split into two groups: those valid for all ledgers (including + * open ones that are still accumulating transactions) and those that are + * only meaningful once the ledger is closed (transaction set finalized). + * + * `LedgerHeader` is embedded inside the `ReadView`/`ApplyView` hierarchy + * and is accessible via `view.info()`. The struct is intentionally small so + * it can be cheaply copied, compared, and transmitted without loading the + * full account-state SHAMap. + * + * @note `validated` is `mutable` because it transitions one-way from + * `false` to `true` and must remain settable even on `const`-qualified + * ledger objects. This is a known design wart. + * + * @see calculateLedgerHash, addRaw, deserializeHeader + */ struct LedgerHeader { explicit LedgerHeader() = default; @@ -18,7 +44,12 @@ struct LedgerHeader // For all ledgers // + /** Monotonically increasing sequence number that identifies this ledger's + * position in the chain. */ LedgerIndex seq = 0; + + /** Close time of the parent (previous) ledger, in `NetClock` seconds + * (epoch: 2000-01-01 00:00:00 UTC). */ NetClock::time_point parentCloseTime; // @@ -26,53 +57,158 @@ struct LedgerHeader // // Closed means "tx set already determined" + + /** This ledger's own identity hash, computed by `calculateLedgerHash`. + * Meaningful only after the ledger is closed. */ uint256 hash = beast::kZERO; + + /** SHAMap root hash of the transaction set for this ledger. */ uint256 txHash = beast::kZERO; + + /** SHAMap root hash of the account-state tree after applying this + * ledger's transaction set. */ uint256 accountHash = beast::kZERO; + + /** Hash of the immediately preceding ledger; links this ledger into the + * chain and is included in the signed hash. */ uint256 parentHash = beast::kZERO; + /** Total XRP in existence at this ledger, in drops (1 XRP = 10^6 drops). */ XRPAmount drops = beast::kZERO; - // If validated is false, it means "not yet validated." - // Once validated is true, it will never be set false at a later time. - // VFALCO TODO Make this not mutable + /** Whether this ledger has been confirmed by a quorum of validators. + * Transitions one-way from `false` to `true`; never reverts. + * Declared `mutable` so it can be set on `const`-qualified objects. */ bool mutable validated = false; + + /** Whether this node has accepted the ledger's transaction set, + * independent of network-wide validation. */ bool accepted = false; - // flags indicating how this ledger close took place + /** Bitmask of close-time flags produced by the consensus round. + * The only defined bit is `kS_LCF_NO_CONSENSUS_TIME` (0x01). + * Serialized as a single `uint8_t`; only the low 8 bits are meaningful. */ int closeFlags = 0; - // the resolution for this ledger close time (2-120 seconds) + /** Granularity to which the close time was rounded, in seconds (2–120). + * Determined by the consensus algorithm and stored per-ledger. */ NetClock::duration closeTimeResolution = {}; - // For closed ledgers, the time the ledger - // closed. For open ledgers, the time the ledger - // will close if there's no transactions. - // + /** For closed ledgers: the time at which the ledger closed, in + * `NetClock` seconds. For open ledgers: the projected close time if + * no transactions arrive. */ NetClock::time_point closeTime; }; -// ledger close flags +/** Close-flag bit set when consensus could not agree on a close time. + * + * Written into `LedgerHeader::closeFlags` during `Ledger::setAccepted()` + * when the `correctCloseTime` argument is `false`. Queried via + * `getCloseAgree()`. + */ static std::uint32_t const kS_LCF_NO_CONSENSUS_TIME = 0x01; +/** Return `true` if the consensus round agreed on a close time for this + * ledger. + * + * Returns `false` when `kS_LCF_NO_CONSENSUS_TIME` is set in + * `info.closeFlags`, which indicates the validator set was unable to + * reach agreement (e.g., significant clock skew or a very small validator + * set). Callers that require a reliable close time — such as the ledger + * replay subsystem — must guard on this value. + * + * @param info The ledger header to inspect. + * @return `true` if `kS_LCF_NO_CONSENSUS_TIME` is not set; `false` + * otherwise. + */ inline bool getCloseAgree(LedgerHeader const& info) { return (info.closeFlags & kS_LCF_NO_CONSENSUS_TIME) == 0; } +/** Append the ledger header to a serializer in canonical network byte order. + * + * Field order (protocol-immutable): `seq` (32-bit), `drops` (64-bit), + * `parentHash`, `txHash`, `accountHash` (each 256-bit), `parentCloseTime`, + * `closeTime` (each 32-bit epoch seconds), `closeTimeResolution` (8-bit), + * `closeFlags` (8-bit). Total: 118 bytes. When `includeHash` is `true`, + * `hash` is appended as an additional 32 bytes. + * + * The hash is omitted by default to avoid circularity: it is derived from + * all other fields and must not be part of the input to + * `calculateLedgerHash`. Pass `includeHash = true` when persisting to the + * node store or transmitting over the wire so receivers can skip + * recomputing it. + * + * @note The field order here must exactly mirror `calculateLedgerHash`. + * They are not mechanically linked; a divergence silently breaks + * consensus-level hash agreement across the network. + * @note `validated` and `accepted` are runtime-only flags and are not + * written to the serializer. + * + * @param info The ledger header to serialize. + * @param s Accumulator that receives the serialized bytes. + * @param includeHash If `true`, append `info.hash` after all other fields. + */ void addRaw(LedgerHeader const&, Serializer&, bool includeHash = false); -/** Deserialize a ledger header from a byte array. */ +/** Deserialize a ledger header from a raw byte buffer. + * + * Reads fields in the same order that `addRaw` writes them. Time fields + * are raw 32-bit epoch counts (seconds since 2000-01-01 00:00:00 UTC) + * and are restored to typed `NetClock::time_point` values. The runtime-only + * fields `validated` and `accepted` are left at their default values. + * + * No semantic validation is performed beyond what `SerialIter` enforces. + * The caller is responsible for verifying that the deserialized `hash` + * matches `calculateLedgerHash` before trusting the data. + * + * @param data View over the raw bytes to deserialize. + * @param hasHash If `true`, read a trailing 256-bit value into + * `LedgerHeader::hash`. Must match how the header was serialized. + * @return A populated `LedgerHeader`. + * @throws std::runtime_error (via `SerialIter`) if @p data is shorter + * than the expected field sequence. + */ LedgerHeader deserializeHeader(Slice data, bool hasHash = false); -/** Deserialize a ledger header (prefixed with 4 bytes) from a byte array. */ +/** Deserialize a ledger header that is preceded by a 4-byte prefix. + * + * Skips the leading `HashPrefix` tag that is prepended when a ledger + * header is stored in the node database or transmitted in a peer-protocol + * message, then delegates to `deserializeHeader`. + * + * @param data View over the raw bytes, including the 4-byte prefix. + * @param hasHash Forwarded to `deserializeHeader`; see its documentation. + * @return A populated `LedgerHeader` as returned by `deserializeHeader`. + * @throws std::runtime_error (via `SerialIter`) if the buffer after + * skipping the prefix is too short. + */ LedgerHeader deserializePrefixedHeader(Slice data, bool hasHash = false); -/** Calculate the hash of a ledger header. */ +/** Compute the canonical 256-bit identity hash for a ledger header. + * + * Feeds all header fields (except `hash` itself, `validated`, and + * `accepted`) into `sha512Half` — the first 256 bits of a SHA-512 digest + * — prepended with `HashPrefix::LedgerMaster` (`LWR\0`). The four-byte + * prefix provides hash-domain separation, preventing collisions with + * hashes computed over other XRPL object types. + * + * Each field is explicitly cast to its protocol-defined wire width before + * hashing, preventing silent integer widening from diverging from the + * network. + * + * @note The field order and widths here must exactly mirror `addRaw`. + * They are not mechanically linked; a divergence causes this node to + * compute hashes that disagree with the rest of the network. + * + * @param info The ledger header to hash. + * @return The 256-bit canonical ledger hash. + */ uint256 calculateLedgerHash(LedgerHeader const& info); diff --git a/include/xrpl/protocol/LedgerShortcut.h b/include/xrpl/protocol/LedgerShortcut.h index 68c31c4c3c..be653f2e53 100644 --- a/include/xrpl/protocol/LedgerShortcut.h +++ b/include/xrpl/protocol/LedgerShortcut.h @@ -2,20 +2,57 @@ namespace xrpl { -/** - * @brief Enumeration of ledger shortcuts for specifying which ledger to use. +/** Symbolic names for the three canonical XRPL ledger states. * - * These shortcuts provide a convenient way to reference commonly used ledgers - * without needing to specify their exact hash or sequence number. + * The XRPL consensus model maintains three distinct ledger states at any + * point in time. Rather than requiring callers to pass magic strings + * (`"current"`, `"closed"`, `"validated"`) or ad-hoc integer sentinels, + * `LedgerShortcut` gives the type system a precise vocabulary for expressing + * ledger-selection intent without a specific sequence number or hash. + * + * In `RPCLedgerHelpers.cpp`, `lookupLedger` parsing maps the JSON strings + * `"current"`, `"closed"`, and `"validated"` onto the corresponding enum + * values before dispatching to the appropriate `getLedger` overload. The + * `AccountTx` RPC handler performs the same mapping when processing the + * `ledger_index` field. The gRPC adapter maps protobuf shortcut constants to + * these values as well. + * + * `LedgerShortcut` also participates as one arm of + * `RelationalDatabase::LedgerSpecifier` — a + * `std::variant` — + * allowing symbolic ledger names to flow through the database query layer via + * `std::visit` dispatch without special-case handling. + * + * @note The scoped `enum class` form prevents implicit integer conversions and + * namespace pollution, both of which are hazards in a codebase that also + * works extensively with raw integer ledger sequence numbers. */ enum class LedgerShortcut { - /** The current working ledger (open, not yet closed) */ + /** The open, in-progress ledger still accumulating new transactions. + * + * This ledger has not been closed or validated, so its contents may + * change. Results derived from it are not final and may be rolled back + * during a reorganisation or consensus failure. + */ Current, - /** The most recently closed ledger (may not be validated) */ + /** The most recently closed ledger; stable in structure but not yet + * consensus-validated. + * + * No new transactions are accepted into this ledger, but the network has + * not yet confirmed it as the authoritative chain tip. It is more stable + * than `Current` but still not suitable for finality guarantees. + */ Closed, - /** The most recently validated ledger */ + /** The most recently validated ledger; the fully consensus-confirmed chain + * tip. + * + * This is the only state considered immutable and trustworthy for finality + * purposes. An RPC node that cannot provide a fresh validated ledger + * (i.e., it is stale) will return an error rather than serve potentially + * incorrect data. + */ Validated }; diff --git a/include/xrpl/protocol/MPTAmount.h b/include/xrpl/protocol/MPTAmount.h index b4907774d2..020b1ac180 100644 --- a/include/xrpl/protocol/MPTAmount.h +++ b/include/xrpl/protocol/MPTAmount.h @@ -1,3 +1,8 @@ +/** @file + * Defines MPTAmount, the canonical signed-integer amount type for + * Multi-Purpose Tokens (MPTs) on the XRP Ledger. + */ + #pragma once #include @@ -13,12 +18,40 @@ namespace xrpl { +/** Typed signed-integer quantity for Multi-Purpose Tokens (MPTs). + * + * MPT balances are plain whole-unit integers — no mantissa/exponent pair, + * no sub-unit naming — capped at `maxMPTokenAmount` (INT64_MAX) by the + * protocol. The class sits alongside `XRPAmount` and `IOUAmount` as one + * of the three concrete amount types that satisfy the `StepAmount` concept + * used by the payment-path and DEX engines. + * + * Arithmetic operators are composed via Boost.Operators (CRTP): + * - `boost::totally_ordered` — synthesizes `!=`, `>`, `>=`, + * `<=` from the declared `==` and `<`. + * - `boost::additive` — synthesizes binary `+`/`-` from + * `+=`/`-=`. + * - `boost::equality_comparable` — heterogeneous `!=` + * from `operator==(value_type)`. + * - `boost::additive` — heterogeneous `+`/`-` with + * raw integers. + * + * Out-of-line `+=`, `-=`, `operator-()`, `==`, and `<` perform no overflow + * detection; callers are responsible for keeping balances in range through + * the ledger constraint machinery. The safe multiplication path + * (`mulRatio`) uses 128-bit intermediates and throws on overflow. + * + * @note `value_` is `protected` (not `private`) to allow subclassing + * without exposing the raw integer to unrelated code. No subclasses + * exist in the current codebase. + */ class MPTAmount : private boost::totally_ordered, private boost::additive, private boost::equality_comparable, private boost::additive { public: + /** Underlying integer type; matches `XRPAmount::value_type`. */ using value_type = std::int64_t; protected: @@ -27,57 +60,149 @@ protected: public: MPTAmount() = default; constexpr MPTAmount(MPTAmount const& other) = default; + + /** Construct a zero amount from the `beast::Zero` sentinel. + * + * Allows idiomatic zero-initialization via `beast::zero` in generic + * code that is templated on amount type. + */ constexpr MPTAmount(beast::Zero); constexpr MPTAmount& operator=(MPTAmount const& other) = default; - // Round to nearest, even on tie. + /** Construct from a `Number`, rounding to nearest with ties to even. + * + * Provides implicit compatibility with XRPL's high-precision arithmetic + * type. The rounding mode matches IEEE 754 default (round-half-to-even). + * + * @param x The `Number` value to convert. + */ explicit MPTAmount(Number const& x) : MPTAmount(static_cast(x)) { } + /** Construct from a raw `int64_t` value. + * + * Explicit to prevent accidental implicit conversion from integers. + * The caller is responsible for ensuring `value` does not exceed + * `maxMPTokenAmount` (INT64_MAX). + * + * @param value The integer amount in whole MPT units. + */ constexpr explicit MPTAmount(value_type value); + /** Assign the `beast::Zero` sentinel, setting the amount to zero. */ constexpr MPTAmount& operator=(beast::Zero); + /** Add `other` to this amount in place. + * + * No overflow detection is performed; callers must ensure the result + * remains within `int64_t` range. + * + * @param other The amount to add. + * @return Reference to `*this` after addition. + */ MPTAmount& operator+=(MPTAmount const& other); + /** Subtract `other` from this amount in place. + * + * No overflow detection is performed; callers must ensure the result + * remains within `int64_t` range. + * + * @param other The amount to subtract. + * @return Reference to `*this` after subtraction. + */ MPTAmount& operator-=(MPTAmount const& other); + /** Return the arithmetic negation of this amount. + * + * Used where a credit and a debit are expressed as equal-magnitude + * amounts of opposite sign before being applied to the ledger. + * Negating `INT64_MIN` is undefined behavior; callers must avoid it. + * + * @return A new `MPTAmount` equal to `-value_`. + */ MPTAmount operator-() const; + /** Test equality with another `MPTAmount`. + * + * Together with `operator<`, satisfies `boost::totally_ordered`, + * from which `!=`, `>`, `<=`, and `>=` are synthesized. + * + * @param other The amount to compare against. + * @return `true` if both amounts hold the same integer value. + */ bool operator==(MPTAmount const& other) const; + /** Test equality with a raw `int64_t` value. + * + * Allows expressions like `amt == 0` without constructing a temporary. + * `boost::equality_comparable` synthesizes the + * mixed-type `!=` from this overload. + * + * @param other The raw integer value to compare against. + * @return `true` if `value_` equals `other`. + */ bool operator==(value_type other) const; + /** Return `true` if this amount is strictly less than `other`. + * + * The single total-order primitive from which `boost::totally_ordered` + * derives `>`, `<=`, and `>=`. Signed comparison gives correct + * semantics for negative balances. + * + * @param other The amount to compare against. + * @return `true` if `value_` is strictly less than `other.value_`. + */ bool operator<(MPTAmount const& other) const; - /** Returns true if the amount is not zero */ + /** Returns true if the amount is not zero. */ explicit constexpr operator bool() const noexcept; + /** Implicit conversion to `Number` for use in high-precision arithmetic. + * + * Allows `MPTAmount` to be passed anywhere a `Number` is expected — + * arithmetic operations, rounding, and comparisons — without an explicit + * cast. The reverse direction (construction from `Number`) is explicit. + */ operator Number() const noexcept { return value(); } - /** Return the sign of the amount */ + /** Return the sign of the amount. + * + * @return `-1` if negative, `0` if zero, `1` if positive. + */ [[nodiscard]] constexpr int signum() const noexcept; - /** Returns the underlying value. Code SHOULD NOT call this - function unless the type has been abstracted away, - e.g. in a templated function. - */ + /** Return the underlying integer value. + * + * Code SHOULD NOT call this function unless the type has been abstracted + * away, e.g. in a templated function. Prefer operating on `MPTAmount` + * directly to keep arithmetic in the typed domain. + * + * @return The raw `int64_t` balance in whole MPT units. + */ [[nodiscard]] constexpr value_type value() const; + /** Return the smallest positive MPT amount (one indivisible unit). + * + * Provides a uniform factory interface shared with `XRPAmount` and + * `IOUAmount` so generic payment-path code can obtain the minimum + * step size without knowing the concrete amount type. + * + * @return `MPTAmount{1}`. + */ static MPTAmount minPositiveAmount(); }; @@ -98,14 +223,12 @@ MPTAmount::operator=(beast::Zero) return *this; } -/** Returns true if the amount is not zero */ constexpr MPTAmount:: operator bool() const noexcept { return value_ != 0; } -/** Return the sign of the amount */ constexpr int MPTAmount::signum() const noexcept { @@ -114,17 +237,13 @@ MPTAmount::signum() const noexcept return (value_ != 0) ? 1 : 0; } -/** Returns the underlying value. Code SHOULD NOT call this - function unless the type has been abstracted away, - e.g. in a templated function. -*/ constexpr MPTAmount::value_type MPTAmount::value() const { return value_; } -// Output MPTAmount as just the value. +/** Stream an `MPTAmount` as its raw integer value. */ template std::basic_ostream& operator<<(std::basic_ostream& os, MPTAmount const& q) @@ -132,12 +251,35 @@ operator<<(std::basic_ostream& os, MPTAmount const& q) return os << q.value(); } +/** Return the decimal string representation of an `MPTAmount`. */ inline std::string to_string(MPTAmount const& amount) { return std::to_string(amount.value()); } +/** Compute `amt * num / den` with configurable rounding direction. + * + * The intermediate product is computed in 128-bit arithmetic to avoid + * overflow when multiplying a 63-bit MPT balance by a 32-bit numerator + * (up to 95 bits required). After division, any remainder is resolved + * based on the sign of `amt` and `roundUp`: + * - Positive amounts round up when `roundUp` is `true`. + * - Negative amounts round away from zero (more negative) when `roundUp` + * is `false`. + * + * Used for fee and reserve calculations that apply percentage-style ratios + * to MPT amounts. + * + * @param amt The base amount to scale. + * @param num Numerator of the ratio (32-bit unsigned). + * @param den Denominator of the ratio (32-bit unsigned, must be > 0). + * @param roundUp If `true`, round the result toward positive infinity; + * if `false`, round toward negative infinity. + * @return The scaled `MPTAmount`. + * @throws std::runtime_error If `den` is zero. + * @throws std::overflow_error If the result exceeds `INT64_MAX`. + */ inline MPTAmount mulRatio(MPTAmount const& amt, std::uint32_t num, std::uint32_t den, bool roundUp) { diff --git a/include/xrpl/protocol/MPTIssue.h b/include/xrpl/protocol/MPTIssue.h index f55029f50d..26cec0405f 100644 --- a/include/xrpl/protocol/MPTIssue.h +++ b/include/xrpl/protocol/MPTIssue.h @@ -5,9 +5,23 @@ namespace xrpl { -/* Adapt MPTID to provide the same interface as Issue. Enables using static - * polymorphism by Asset and other classes. MPTID is a 192-bit concatenation - * of a 32-bit account sequence and a 160-bit account id. +/** Identifies a Multi-Purpose Token issuance, adapting `MPTID` to mirror + * the public interface of `Issue`. + * + * `MPTIssue` wraps a single 192-bit `MPTID` (32-bit big-endian sequence + * concatenated with a 160-bit `AccountID`) and exposes the same accessors as + * `Issue` — `getIssuer()`, `getText()`, `setJson()`, `native()`, and + * `integral()` — allowing `Asset` and any template code constrained by + * `ValidIssueType` to treat MPTs and IOUs uniformly. + * + * Key semantic differences from `Issue`: + * - `native()` always returns `false` (MPTs are never the native currency). + * - `integral()` always returns `true` (MPT amounts are 64-bit integers, + * unlike IOUs which use multi-precision rational arithmetic). + * - Equality and ordering compare the full 192-bit `MPTID`, with no + * special-case equivalence class for any sentinel value. + * + * @see Issue, Asset, MPTID */ class MPTIssue { @@ -17,27 +31,80 @@ private: public: MPTIssue() = default; + /** Constructs an MPTIssue from a pre-formed 192-bit issuance identifier. + * + * @param issuanceID The packed MPTID (32-bit sequence ‖ 160-bit AccountID). + */ MPTIssue(MPTID const& issuanceID); + /** Constructs an MPTIssue from the raw components of an MPTID. + * + * Delegates to `xrpl::makeMptID(sequence, account)` to assemble the + * packed MPTID, saving callers from invoking that helper explicitly. + * + * @param sequence The issuer's account sequence number at the time of + * issuance. + * @param account The AccountID of the issuer. + */ MPTIssue(std::uint32_t sequence, AccountID const& account); + /** Implicit conversion to the underlying `MPTID`. + * + * Allows an `MPTIssue` to be passed wherever a raw `MPTID` is expected + * without an explicit cast. + * + * @return A reference to the underlying `MPTID`, valid for the lifetime + * of this object. + */ operator MPTID const&() const { return mptID_; } + /** Extracts the issuer's `AccountID` from the packed `MPTID`. + * + * `MPTID` lays out 4 bytes of sequence followed immediately by 20 bytes + * of `AccountID`. This method returns a reference into that buffer by + * pointer-casting past the leading 4 bytes. A `static_assert` on the + * total size of `MPTID` guards against layout changes breaking this + * assumption at compile time. + * + * @return A reference to the `AccountID` embedded in the `MPTID`. + * Valid for the lifetime of this `MPTIssue` object. + * @note The free function `getMPTIssuer()` achieves the same extraction + * via `std::bit_cast` and returns by value, avoiding any + * lifetime dependency on the source object. + */ [[nodiscard]] AccountID const& getIssuer() const; + /** Returns the underlying `MPTID`. + * + * @return A reference to the 192-bit issuance identifier, valid for the + * lifetime of this object. + */ [[nodiscard]] constexpr MPTID const& getMptID() const { return mptID_; } + /** Returns the hex string representation of the underlying `MPTID`. + * + * @return Uppercase hex encoding of the 192-bit issuance identifier. + */ [[nodiscard]] std::string getText() const; + /** Writes the issuance identifier into a JSON object under the key + * `mpt_issuance_id`. + * + * Serializes the `MPTID` as a hex string. Contrasted with + * `Issue::setJson()`, which writes separate `currency` and `issuer` keys. + * + * @param jv Output JSON object to write into; existing keys are not + * cleared. + */ void setJson(json::Value& jv) const; @@ -47,12 +114,30 @@ public: friend constexpr std::weak_ordering operator<=>(MPTIssue const& lhs, MPTIssue const& rhs); + /** Returns `false`; MPTs are never the native asset (XRP). + * + * Mirrors `Issue::native()` so that generic code can query XRP-ness + * without a type dispatch. `Asset::getAmountType()` relies on this flag + * to select `XRPAmount` vs `IOUAmount` vs `MPTAmount` at compile time. + * + * @return Always `false`. + */ static bool native() { return false; } + /** Returns `true`; MPT amounts are stored as 64-bit integers. + * + * Mirrors the naming of `Issue::integral()` so that generic code can + * distinguish integer (drop/MPT) amounts from multi-precision IOU + * amounts without a type dispatch. Unlike `Issue::integral()`, this is + * unconditionally `true` — all MPTs use integer arithmetic regardless of + * the token configuration. + * + * @return Always `true`. + */ static bool integral() { @@ -60,19 +145,44 @@ public: } }; +/** Returns `true` if two `MPTIssue` instances represent the same issuance. + * + * Delegates to the full 192-bit comparison of the underlying `MPTID`s. + * Both the sequence number and the issuer `AccountID` must match; there is + * no partial-equality exemption as there is in `Issue::operator==` for XRP. + * + * @param lhs Left-hand issuance. + * @param rhs Right-hand issuance. + * @return `true` iff both `MPTID`s are bitwise equal. + */ constexpr bool operator==(MPTIssue const& lhs, MPTIssue const& rhs) { return lhs.mptID_ == rhs.mptID_; } +/** Provides a strict weak ordering over `MPTIssue` values. + * + * Delegates to the 192-bit lexicographic comparison of the underlying + * `MPTID`s. The ordering is consistent with `operator==`: two issuances are + * equivalent iff their full `MPTID`s are identical. + * + * @param lhs Left-hand issuance. + * @param rhs Right-hand issuance. + * @return A `std::weak_ordering` value consistent with `operator==`. + */ constexpr std::weak_ordering operator<=>(MPTIssue const& lhs, MPTIssue const& rhs) { return lhs.mptID_ <=> rhs.mptID_; } -/** MPT is a non-native token. +/** Returns `false`; an `MPTID` never identifies the native XRP asset. + * + * Provides the same naming convention as `isXRP(Issue)`, allowing call + * sites to test XRP-ness uniformly across both issue types. + * + * @return Always `false`. */ inline bool isXRP(MPTID const&) @@ -80,6 +190,21 @@ isXRP(MPTID const&) return false; } +/** Extracts the issuer `AccountID` from a `MPTID` by value. + * + * Copies the 20 bytes that follow the leading 4-byte sequence field into a + * temporary array, then uses `std::bit_cast` to reinterpret them as an + * `AccountID`. The `static_assert` on the total size of `MPTID` ensures the + * layout assumption holds; if the type ever gains padding the build fails. + * `std::bit_cast` is typically optimized to nothing in the final assembly. + * + * @param mptid The 192-bit issuance identifier to extract from. + * @return The `AccountID` embedded in bytes 4–23 of `mptid`. + * @note Use `MPTIssue::getIssuer()` when a zero-copy reference into an + * existing `MPTIssue` object is sufficient. The rvalue overloads of + * this function are deleted to prevent dangling references from + * temporaries. + */ inline AccountID getMPTIssuer(MPTID const& mptid) { @@ -93,12 +218,21 @@ getMPTIssuer(MPTID const& mptid) return std::bit_cast(bytes); } -// Disallow temporary +// Deleted to prevent a dangling-reference bug: if a temporary MPTID were +// accepted, the returned AccountID const& would immediately dangle. AccountID const& getMPTIssuer(MPTID const&&) = delete; AccountID const& getMPTIssuer(MPTID&&) = delete; +/** Returns the `MPTID` sentinel representing "no MPT". + * + * Encodes `{ sequence=0, account=noAccount() }` — all-zero bits. + * Mirrors `noIssue()` in `Issue.h` for use in contexts where a + * missing or invalid MPT must be represented without `std::optional`. + * + * @return The all-zero 192-bit sentinel `MPTID`. + */ inline MPTID noMPT() { @@ -106,6 +240,15 @@ noMPT() return kMPT.getMptID(); } +/** Returns the `MPTID` sentinel representing a structurally invalid MPT. + * + * Encodes `{ sequence=0, account=xrpAccount() }` — sequence zero with the + * XRP account address as issuer, which is a conventionally invalid issuer + * for MPTs. `Asset`'s `BadAsset` comparison detects this sentinel by + * checking `getIssuer() == xrpAccount()`. + * + * @return The sentinel `MPTID` whose issuer is `xrpAccount()`. + */ inline MPTID badMPT() { @@ -113,6 +256,15 @@ badMPT() return kMPT.getMptID(); } +/** Appends the underlying `MPTID` to a hasher. + * + * Plugs `MPTIssue` into the Beast hashing framework, enabling use in + * Beast-aware hash maps and sets. + * + * @tparam Hasher A type satisfying the `beast::hash_append` concept. + * @param h The hasher to append to. + * @param r The issuance whose `MPTID` is appended. + */ template void hash_append(Hasher& h, MPTIssue const& r) @@ -121,15 +273,49 @@ hash_append(Hasher& h, MPTIssue const& r) hash_append(h, r.getMptID()); } +/** Returns the canonical wire-format JSON representation of an MPT issuance. + * + * Convenience wrapper around `MPTIssue::setJson()`. The returned object + * contains a single `mpt_issuance_id` field with the hex-encoded `MPTID`. + * + * @param mptIssue The issuance to serialize. + * @return A JSON object of the form `{"mpt_issuance_id": ""}`. + */ json::Value toJson(MPTIssue const& mptIssue); +/** Returns the hex string representation of an MPT issuance. + * + * @param mptIssue The issuance to convert. + * @return Uppercase hex encoding of the underlying 192-bit `MPTID`. + */ std::string to_string(MPTIssue const& mptIssue); +/** Parses an MPT issuance from a JSON object. + * + * Validates in strict order: `v` must be a JSON object; `currency` and + * `issuer` keys must be absent (their presence indicates IOU data routed + * to the wrong parser); `mpt_issuance_id` must be a string containing a + * valid 48-character hex-encoded `MPTID`. + * + * @param jv The JSON value to parse; must be an object. + * @return The parsed `MPTIssue`. + * @throws std::runtime_error if `jv` is not a JSON object, or if `currency` + * or `issuer` keys are present. + * @throws json::Error if `mpt_issuance_id` is absent, not a string, or not + * a valid 192-bit hex value. + * @see toJson for the inverse operation. + */ MPTIssue mptIssueFromJson(json::Value const& jv); +/** Writes the hex representation of an MPT issuance to a stream. + * + * @param os The output stream. + * @param x The issuance to write. + * @return `os`, to allow chaining. + */ std::ostream& operator<<(std::ostream& os, MPTIssue const& x); @@ -137,6 +323,13 @@ operator<<(std::ostream& os, MPTIssue const& x); namespace std { +/** Specializes `std::hash` for `xrpl::MPTID`, delegating to the type's own + * hasher. + * + * Enables `MPTID` to be used directly as a key in `std::unordered_map`, + * `std::unordered_set`, and similar standard containers without wrapping + * in `MPTIssue`. + */ template <> struct hash : xrpl::MPTID::hasher { diff --git a/include/xrpl/protocol/MultiApiJson.h b/include/xrpl/protocol/MultiApiJson.h index 5a7dfcd731..2cbba1f8c2 100644 --- a/include/xrpl/protocol/MultiApiJson.h +++ b/include/xrpl/protocol/MultiApiJson.h @@ -1,3 +1,12 @@ +/** @file + * Holds one pre-built `Json::Value` per supported API version so that a + * single ledger event can be delivered to subscribers speaking different API + * versions without re-serializing on every send. + * + * The public alias `xrpl::MultiApiJson` binds the template to the live + * version range `[kAPI_MINIMUM_SUPPORTED_VERSION, kAPI_MAXIMUM_VALID_VERSION]`. + */ + #pragma once #include @@ -14,6 +23,15 @@ namespace xrpl { namespace detail { + +/** Variable template that is `true` only for lvalue-reference-qualified + * `std::integral_constant` specializations (both cv-variants). + * + * Used as the building block for the `some_integral_constant` concept, which + * disambiguates the compile-time and runtime overloads of `VisitorT::operator()`. + * + * @tparam T The type to test. + */ template constexpr bool kIS_INTEGRAL_CONSTANT = false; template @@ -21,35 +39,94 @@ constexpr bool kIS_INTEGRAL_CONSTANT&> = true; template constexpr bool kIS_INTEGRAL_CONSTANT const&> = true; +/** Concept satisfied only by `std::integral_constant` specializations (lvalue refs). + * + * Used in `requires` clauses on `VisitorT::operator()` to prevent the + * runtime-`unsigned` overloads from being selected when a compile-time + * constant is passed, avoiding otherwise-ambiguous partial ordering. + * + * @tparam T The type to constrain. + */ template concept some_integral_constant = detail::kIS_INTEGRAL_CONSTANT; -// This class is designed to wrap a collection of _almost_ identical json::Value -// objects, indexed by version (i.e. there is some mapping of version to object -// index). It is used e.g. when we need to publish JSON data to users supporting -// different API versions. We allow manipulation and inspection of all objects -// at once with `isMember` and `set`, and also individual inspection and updates -// of an object selected by the user by version, using `visitor_t` nested type. +/** Holds one `Json::Value` per API version in a fixed-size array, enabling + * single-pass event serialization for multi-version subscriber delivery. + * + * When an XRPL server event (e.g., a validated transaction) must be published + * to subscribers that may speak different API versions, re-serializing or + * branching inside the send path would add latency proportional to subscriber + * count. `MultiApiJson` amortizes version-specific transformations to once per + * event: callers construct the object from a common base `Json::Value`, apply + * per-version mutations via `visit`, and then each subscriber's delivery path + * calls `visit(apiVersion, sender)` to pick the pre-built slot cheaply. + * + * The array has `MaxVer + 1 - MinVer` elements; version `v` maps to index + * `v - MinVer`. `set` and `isMember` operate across all slots; `visit` + * operates on a single slot selected by version. + * + * @note Prefer the `xrpl::MultiApiJson` type alias over instantiating this + * template directly. Direct instantiation is intended for tests only; all + * production code should use the alias, which is bound to the live version + * constants and automatically tracks any future version-range changes. + * + * @tparam MinVer Minimum (inclusive) supported API version. + * @tparam MaxVer Maximum (inclusive) supported API version. + */ template struct MultiApiJson { static_assert(MinVer <= MaxVer); + /** Returns `true` if `v` falls within `[MinVer, MaxVer]`. + * + * Used by `VisitorT` to guard against out-of-range version accesses. + * @param v The API version number to test. + * @return `true` iff `v` is a valid slot index. + */ static constexpr auto valid(unsigned int v) noexcept -> bool { return v >= MinVer && v <= MaxVer; } + /** Maps an API version number to its zero-based array slot. + * + * Out-of-range values below `MinVer` clamp to 0 rather than underflowing; + * the caller is responsible for checking `valid(v)` before trusting the + * result. Values above `MaxVer` are not clamped — `valid()` must be used + * to guard against those. + * + * @param v The API version number to map. + * @return The corresponding index into `val`. + */ static constexpr auto index(unsigned int v) noexcept -> std::size_t { return (v < MinVer) ? 0 : static_cast(v - MinVer); } + /** Number of API version slots stored; equals `MaxVer + 1 - MinVer`. */ constexpr static std::size_t kSIZE = MaxVer + 1 - MinVer; + + /** The per-version JSON values, indexed by `index(version)`. + * + * Public to allow direct slot access in tests and for `VisitorT` (which + * is a friend via the `static constexpr` data member). Production callers + * should use `set`, `isMember`, and `visit` rather than indexing directly. + */ std::array val = {}; + /** Constructs the object, optionally copy-initializing every slot. + * + * When `init` is the default (null) `Json::Value`, all slots remain + * default-initialized (null). When a non-null value is supplied, every + * slot is copy-initialized to it. The common pattern in `NetworkOPs.cpp` + * is to pass a shared base object and then apply per-version mutations + * via `visit`. + * + * @param init Base value to copy into every slot; omit for null slots. + */ explicit MultiApiJson(json::Value const& init = {}) { if (init == json::Value{}) @@ -58,6 +135,16 @@ struct MultiApiJson v = init; } + /** Writes a key-value pair into every slot simultaneously. + * + * Use for fields that are identical across all API versions — the majority + * of transaction fields. Cheaper than calling `visit` once per version for + * shared data. The `requires` clause restricts `v` to types from which + * `Json::Value` can be constructed, preventing silent misuse. + * + * @param key The JSON object key to set. + * @param v The value to assign; must be constructible to `Json::Value`. + */ void set(char const* key, auto const& v) requires std::constructible_from @@ -66,8 +153,26 @@ struct MultiApiJson a[key] = v; } - enum class IsMemberResult : int { None = 0, Some, All }; + /** Tri-state result of `isMember`: indicates how many version slots contain a key. + * + * Scoped to `MultiApiJson` rather than a separate class enum deliberately — + * the struct is narrow enough to serve as its own scope for this result. + */ + enum class IsMemberResult : int { + None = 0, /**< No slot contains the key. */ + Some, /**< At least one but not all slots contain the key. */ + All /**< Every slot contains the key. */ + }; + /** Queries how many version slots contain the given JSON key. + * + * Useful for asserting that version-specific mutations were (or were not) + * applied before delivery. `NetworkOPs` uses it in assertions to verify + * that certain fields are never set on a freshly-constructed object. + * + * @param key The JSON object key to look up in each slot. + * @return `IsMemberResult::None`, `Some`, or `All`. + */ [[nodiscard]] IsMemberResult isMember(char const* key) const { @@ -83,6 +188,33 @@ struct MultiApiJson return count < kSIZE ? IsMemberResult::Some : IsMemberResult::All; } + /** Stateless callable that routes invocations to the correct version slot. + * + * Provides four `operator()` overloads split along two axes: + * + * 1. **Compile-time version** (`std::integral_constant`): + * the version is checked with `static_assert`; the JSON reference and + * optional extra arguments are forwarded to `fn` at compile time. + * + * 2. **Runtime version** (any type convertible to `unsigned` that is + * *not* an `integral_constant`): the version is checked with + * `XRPL_ASSERT`; the `some_integral_constant` concept in the `requires` + * clause prevents these overloads from being selected when a + * compile-time constant is passed, resolving the otherwise-ambiguous + * partial ordering. + * + * Each axis is further split by whether extra arguments are forwarded to + * `fn` after the `Json::Value` (and possibly the version value). This + * matches the calling convention of `forAllApiVersions`/`forApiVersions`, + * which pass each version as an `integral_constant` plus any extra args + * bound at the call site. + * + * `const`-propagation is automatic: the JSON reference passed to `fn` + * mirrors the `const`-ness of the `Json&` parameter. + * + * @note Exposed as `kVISITOR` to allow direct testing; prefer `visit()` + * for all production call sites. + */ static constexpr struct VisitorT final { // integral_constant version, extra arguments @@ -145,6 +277,19 @@ struct MultiApiJson } } kVISITOR = {}; + /** Returns a closure that dispatches `kVISITOR` for this object (mutable). + * + * The returned callable captures `this` and forwards all arguments to + * `kVISITOR`. This form is composable with `forAllApiVersions` and + * `forApiVersions`: those utilities iterate the version range at compile + * time, passing each version as an `integral_constant`. The closure + * satisfies that calling convention exactly, so + * `forAllApiVersions(obj.visit(), lambda)` iterates every version with a + * single consistent lambda without any per-version conditional logic. + * + * @return A lambda `(auto... args) -> auto` that calls + * `kVISITOR(*this, args...)`. + */ auto visit() { @@ -155,6 +300,16 @@ struct MultiApiJson { return kVISITOR(*self, std::forward(args)...); }; } + /** Returns a closure that dispatches `kVISITOR` for this object (const). + * + * Identical to the mutable overload but captures `this` as `const`, + * propagating const-ness through to the `Json::Value` reference passed to + * the callable. Used when the caller only needs to read the pre-built JSON + * (e.g., subscriber delivery in `BookListeners::publish`). + * + * @return A lambda `(auto... args) -> auto` that calls + * `kVISITOR(*this, args...)` on the const object. + */ [[nodiscard]] auto visit() const { @@ -165,6 +320,20 @@ struct MultiApiJson { return kVISITOR(*self, std::forward(args)...); }; } + /** Directly invokes `kVISITOR` for a single version (mutable). + * + * Equivalent to `visit()(args...)` but avoids the closure allocation. + * Typical usage: + * ```cpp + * jvObj.visit(RPC::kAPI_VERSION<1>, [](Json::Value& jv) { + * jv["ledger_index"] = std::to_string(jv["ledger_index"].asInt()); + * }); + * ``` + * + * @param args Version (compile-time or runtime) followed by a callable + * and any extra arguments accepted by `kVISITOR`. + * @return The return value of the callable. + */ template auto visit(Args... args) -> std::invoke_result_t @@ -174,6 +343,20 @@ struct MultiApiJson return kVISITOR(*this, std::forward(args)...); } + /** Directly invokes `kVISITOR` for a single version (const). + * + * Const counterpart of the mutable `visit(args...)` overload. Used when + * the JSON slot must not be mutated — for example in the subscriber + * delivery path where each subscriber picks its pre-built slot: + * ```cpp + * jvObj.visit(subscriber->getApiVersion(), + * [&](Json::Value const& jv) { subscriber->send(jv, true); }); + * ``` + * + * @param args Version (compile-time or runtime) followed by a callable + * and any extra arguments accepted by `kVISITOR`. + * @return The return value of the callable. + */ template [[nodiscard]] auto visit(Args... args) const -> std::invoke_result_t @@ -186,7 +369,15 @@ struct MultiApiJson } // namespace detail -// Wrapper for Json for all supported API versions. +/** Holds one pre-built `Json::Value` per currently supported API version. + * + * Bound to `[kAPI_MINIMUM_SUPPORTED_VERSION, kAPI_MAXIMUM_VALID_VERSION]` + * (currently versions 1–3), so the concrete type stores exactly three + * `Json::Value` objects. Changing those constants automatically resizes + * every `MultiApiJson` instance in the server. + * + * @see detail::MultiApiJson for the full behavioral contract. + */ using MultiApiJson = detail::MultiApiJson; diff --git a/include/xrpl/protocol/NFTSyntheticSerializer.h b/include/xrpl/protocol/NFTSyntheticSerializer.h index a1d8bce985..da952c6dcf 100644 --- a/include/xrpl/protocol/NFTSyntheticSerializer.h +++ b/include/xrpl/protocol/NFTSyntheticSerializer.h @@ -1,3 +1,20 @@ +/** @file + * Aggregator entry point for injecting synthetic NFT fields into RPC + * transaction responses. + * + * "Synthetic" fields (`nftoken_ids`, `nftoken_id`, `offer_id`) are derived + * at query time from the ledger state changes recorded in `TxMeta`; they are + * not stored on-chain. Callers invoke a single function here rather than + * calling the individual NFT inserters directly, keeping call sites from + * accumulating an ever-growing list of per-type injector calls as new NFT + * transaction types are added. + * + * The underlying extraction helpers (`insertNFTokenID`, `insertNFTokenOfferID`) + * live in `NFTokenID.h` and `NFTokenOfferID.h` under the broader `xrpl::` + * namespace so that Clio (the XRPL History API server) can call those helpers + * directly without the `xrpl::RPC` coupling imposed by this header. + */ + #pragma once #include @@ -8,13 +25,44 @@ namespace xrpl::RPC { -/** - Adds common synthetic fields to transaction-related JSON responses - - @{ +/** Enrich a transaction JSON response with NFT-derived synthetic fields. + * + * Delegates to two independent inserters, in order: + * + * - `insertNFTokenID` — adds `nftoken_id` (for `NFTokenMint` and + * `NFTokenAcceptOffer`) or `nftoken_ids` (for `NFTokenCancelOffer`) by + * diffing the NFToken arrays across all affected ledger nodes recorded in + * the transaction metadata. + * - `insertNFTokenOfferID` — adds `offer_id` for `NFTokenCreateOffer` (and + * mints that include an immediate sell offer) by locating the newly created + * `NFTokenOffer` node and extracting its `sfLedgerIndex`. + * + * Both delegates gate themselves on transaction type and `tesSUCCESS`, so + * this function is safe to call for any transaction type: non-NFT + * transactions produce no output. + * + * Synthetic fields are written into `response[jss::meta]`. The `meta` + * sub-object should already be populated by the caller (e.g., via + * `TxMeta::getJson`) before this function is invoked — consistent with the + * call-site pattern in `Tx.cpp`, `Simulate.cpp`, `AccountTx.cpp`, and + * `NetworkOPs.cpp`, where this call appears alongside `insertDeliveredAmount` + * and `insertMPTokenIssuanceID` as part of a fixed metadata-enrichment + * sequence. + * + * @param response Top-level RPC response object. Synthetic fields are + * written into its `meta` sub-object, which is created on demand if + * absent. + * @param transaction The executed transaction. A null pointer is handled + * gracefully by the delegates (no-op). + * @param transactionMeta Read-only view of the transaction's metadata used to + * diff ledger node states and locate newly created objects. + * + * @see xrpl::insertNFTokenID, xrpl::insertNFTokenOfferID */ void -insertNFTSyntheticInJson(json::Value&, std::shared_ptr const&, TxMeta const&); -/** @} */ +insertNFTSyntheticInJson( + json::Value& response, + std::shared_ptr const& transaction, + TxMeta const& transactionMeta); } // namespace xrpl::RPC diff --git a/include/xrpl/protocol/NFTokenID.h b/include/xrpl/protocol/NFTokenID.h index f61c6bd5cb..0f6f988f60 100644 --- a/include/xrpl/protocol/NFTokenID.h +++ b/include/xrpl/protocol/NFTokenID.h @@ -1,3 +1,16 @@ +/** @file + * Helpers that reconstruct NFToken identities from transaction metadata + * and inject them into RPC JSON responses as synthetic fields. + * + * Raw ledger metadata records before/after state of `NFTokenPage` objects + * but does not directly annotate which token was created or consumed. The + * functions below bridge that gap. They are free (non-static) functions so + * that Clio (the XRPL History API server) can link against them directly + * and perform the same enrichment without duplicating the logic. + * + * @see NFTokenOfferID.h for the analogous helpers for `NFTokenOffer` IDs. + */ + #pragma once #include @@ -11,28 +24,100 @@ namespace xrpl { -/** - Add a `nftoken_ids` field to the `meta` output parameter. - The field is only added to successful NFTokenMint, NFTokenAcceptOffer, - and NFTokenCancelOffer transactions. - - Helper functions are not static because they can be used by Clio. - @{ +/** Returns true if this transaction could have produced or consumed an NFToken. + * + * Acts as a cheap early-exit guard for all downstream extraction logic. + * A transaction qualifies only when it is one of the three NFT types + * (`ttNFTOKEN_MINT`, `ttNFTOKEN_ACCEPT_OFFER`, `ttNFTOKEN_CANCEL_OFFER`) + * and its result code is `tesSUCCESS`. A failed transaction cannot have + * mutated any NFToken page, so metadata diffing would be meaningless. + * + * @param serializedTx The executed transaction; a null pointer yields + * false immediately. + * @param transactionMeta Metadata from the same transaction, used to + * check the result code. + * @return True only when `serializedTx` is non-null, its type is one of + * the three NFT transaction types, and the result is `tesSUCCESS`. */ bool canHaveNFTokenID(std::shared_ptr const& serializedTx, TxMeta const& transactionMeta); +/** Recovers the ID of the NFToken added by a mint transaction. + * + * `ttNFTOKEN_MINT` metadata records the full token arrays of every + * affected `NFTokenPage` in `sfPreviousFields` and `sfFinalFields` but + * does not tag the newly inserted entry. This function recovers it by + * set-difference: it accumulates token IDs from all previous states into + * `prevIDs` and all final states into `finalIDs`, then uses + * `std::mismatch` to locate the first entry present in `finalIDs` but + * absent from `prevIDs`. Because `NFTokenPage` entries are stored in + * sorted order by token ID, both vectors are already ordered and + * `std::mismatch` finds the insertion point in linear time without + * additional sorting. + * + * @note When a mint causes an existing page to split, the linked-list + * rewiring may produce a `sfModifiedNode` for a third page whose + * `sfPreviousFields` contain only pointer updates (`NextPageMin` / + * `PreviousPageMin`) with no `sfNFTokens` array. Such nodes are + * skipped silently; without this guard the size invariant below + * would incorrectly fail for legitimate mints. + * + * @param transactionMeta Metadata from a `ttNFTOKEN_MINT` transaction. + * @return The `uint256` ID of the newly minted token, or `std::nullopt` + * if `finalIDs.size() != prevIDs.size() + 1` (tokens are minted one + * at a time) or if `std::mismatch` unexpectedly reaches the end of + * `finalIDs`. + */ std::optional getNFTokenIDFromPage(TxMeta const& transactionMeta); +/** Collects the NFToken IDs referenced by deleted `NFTokenOffer` objects. + * + * Both `ttNFTOKEN_ACCEPT_OFFER` and `ttNFTOKEN_CANCEL_OFFER` delete one + * or more `ltNFTOKEN_OFFER` ledger entries. Each deleted offer's + * `sfFinalFields` carries the `sfNFTokenID` it was created for, so the + * token identity is recoverable without set-difference arithmetic. + * Results are sorted and deduplicated because a single cancel transaction + * can target multiple offers that reference the same underlying NFT. + * + * @param transactionMeta Metadata from a `ttNFTOKEN_ACCEPT_OFFER` or + * `ttNFTOKEN_CANCEL_OFFER` transaction. + * @return Sorted, deduplicated vector of `uint256` NFToken IDs recovered + * from all deleted offer nodes; empty if no qualifying deletions are + * found. + */ std::vector getNFTokenIDFromDeletedOffer(TxMeta const& transactionMeta); +/** Injects synthetic NFToken ID field(s) into an RPC transaction response. + * + * Calls `canHaveNFTokenID` first; returns immediately without modifying + * `response` if the transaction is ineligible or extraction yields nothing. + * When eligible, dispatches by transaction type: + * + * - `ttNFTOKEN_MINT` — writes `jss::nftoken_id` (single string) derived + * from `getNFTokenIDFromPage`. + * - `ttNFTOKEN_ACCEPT_OFFER` — writes `jss::nftoken_id` (single string, + * first element) derived from `getNFTokenIDFromDeletedOffer`. + * - `ttNFTOKEN_CANCEL_OFFER` — writes `jss::nftoken_ids` (JSON array of + * all deduplicated IDs) derived from `getNFTokenIDFromDeletedOffer`. + * + * The singular/plural field-name distinction reflects a real semantic + * difference: accept and mint affect exactly one NFT, while cancel can + * affect many. + * + * @param response The JSON object to enrich; fields are written + * directly into it. The caller is responsible for scoping this to + * the `jss::meta` sub-object of the full response. + * @param transaction The executed transaction. A null pointer is + * handled gracefully via `canHaveNFTokenID`. + * @param transactionMeta Read-only metadata used for eligibility checking + * and token ID extraction. + */ void insertNFTokenID( json::Value& response, std::shared_ptr const& transaction, TxMeta const& transactionMeta); -/** @} */ } // namespace xrpl diff --git a/include/xrpl/protocol/NFTokenOfferID.h b/include/xrpl/protocol/NFTokenOfferID.h index c4a80356bf..92dfc2b2d6 100644 --- a/include/xrpl/protocol/NFTokenOfferID.h +++ b/include/xrpl/protocol/NFTokenOfferID.h @@ -1,3 +1,20 @@ +/** @file + * Helpers that recover the ledger index of a newly created `NFTokenOffer` + * from transaction metadata and inject it into RPC JSON responses as a + * synthetic `offer_id` field. + * + * The XRPL transaction format records only inputs; the ledger index of a + * newly created offer object appears solely in the `CreatedNode` entries of + * the transaction metadata. The three functions below encapsulate the scan + * once so that every API consumer — rippled RPC handlers and Clio alike — + * can obtain the offer ID without walking `AffectedNodes` manually. All + * three functions are free (non-static) so that Clio can call them directly + * without duplicating the logic. + * + * @see NFTokenID.h for the analogous helpers that inject `nftoken_id` / + * `nftoken_ids` for mint, accept-offer, and cancel-offer operations. + */ + #pragma once #include @@ -10,26 +27,76 @@ namespace xrpl { -/** - Add an `offer_id` field to the `meta` output parameter. - The field is only added to successful NFTokenCreateOffer transactions. - - Helper functions are not static because they can be used by Clio. - @{ +/** Determine whether a transaction can have an NFToken offer ID. + * + * Acts as a cheap pre-filter before the metadata scan in + * `getOfferIDFromCreatedOffer`. Three conditions must all hold: + * + * 1. `serializedTx` is non-null. + * 2. The transaction type is `ttNFTOKEN_CREATE_OFFER`, **or** it is + * `ttNFTOKEN_MINT` with `sfAmount` present (a mint that simultaneously + * creates an immediate-sale offer). + * 3. The transaction succeeded (`tesSUCCESS`). A failed transaction never + * modifies the ledger, so no offer object can exist in the metadata. + * + * @param serializedTx The transaction to inspect. A null `shared_ptr` + * is handled safely and causes the function to return `false`. + * @param transactionMeta Metadata whose result code is checked for success. + * @return `true` only when all three conditions are satisfied, indicating + * that a subsequent call to `getOfferIDFromCreatedOffer` may yield a + * value. */ bool canHaveNFTokenOfferID( std::shared_ptr const& serializedTx, TxMeta const& transactionMeta); +/** Extract the ledger index of the NFToken offer created by a transaction. + * + * Scans the `AffectedNodes` array in `transactionMeta` for a `CreatedNode` + * whose `sfLedgerEntryType` is `ltNFTOKEN_OFFER`. Modified and deleted + * nodes are skipped. The first qualifying node's `sfLedgerIndex` is + * returned; because at most one `NFTokenOffer` can be created per + * transaction, the loop exits immediately on the first match. + * + * @param transactionMeta Read-only transaction metadata to scan. + * @return The `uint256` ledger index of the newly created offer, or + * `std::nullopt` if no `CreatedNode` of type `ltNFTOKEN_OFFER` is + * found. Absence is a plausible non-exceptional condition (e.g., when + * processing historical or externally sourced transactions with + * incomplete metadata), not an error. + * + * @note Callers that have already performed their own eligibility checks + * (e.g., Clio) may call this function directly without first calling + * `canHaveNFTokenOfferID`. + */ std::optional getOfferIDFromCreatedOffer(TxMeta const& transactionMeta); +/** Inject the NFToken offer ID into a JSON response as `jss::offer_id`. + * + * Composes `canHaveNFTokenOfferID` and `getOfferIDFromCreatedOffer`: + * returns immediately without touching `response` if the transaction is + * ineligible or the metadata contains no created offer node. When an offer + * ID is successfully extracted, it is written into `response[jss::offer_id]` + * as a hex string. + * + * The primary call site is `xrpl::RPC::insertNFTSyntheticInJson`, which + * passes `response[jss::meta]` as the target so that `offer_id` appears + * inside the `meta` sub-object alongside the raw node data. + * + * @param response The JSON object to enrich; `jss::offer_id` is + * written directly into it on success. The caller is responsible for + * scoping this to `jss::meta` of the full response. + * @param transaction The executed transaction. A null pointer is + * handled gracefully (no-op) via `canHaveNFTokenOfferID`. + * @param transactionMeta Read-only transaction metadata used to locate the + * created `NFTokenOffer` node. + */ void insertNFTokenOfferID( json::Value& response, std::shared_ptr const& transaction, TxMeta const& transactionMeta); -/** @} */ } // namespace xrpl diff --git a/include/xrpl/protocol/PathAsset.h b/include/xrpl/protocol/PathAsset.h index 4c4a3f7af4..1a0d2e456e 100644 --- a/include/xrpl/protocol/PathAsset.h +++ b/include/xrpl/protocol/PathAsset.h @@ -1,3 +1,12 @@ +/** @file + * Defines `PathAsset`, the token identifier for a single hop in an XRPL + * payment path, and its associated free functions. + * + * `PathAsset` holds `std::variant` — just the *which + * currency or MPT* component of a path element, without the issuer. + * This is narrower than `Asset` (`std::variant`) because + * `STPathElement` records the issuer in a separate field. + */ #pragma once #include @@ -5,7 +14,19 @@ namespace xrpl { -/* Represent STPathElement's asset, which can be Currency or MPTID. +/** Token identifier for a single hop within an XRPL payment path. + * + * Holds `std::variant` — the *which currency/MPT* component + * of a path element, without the issuer. Issuers are stored separately in + * `STPathElement::mIssuerID` because payment-path serialization records them + * as independent fields; folding them into `PathAsset` would duplicate data + * and complicate encoding. + * + * This is intentionally narrower than `Asset`, which pairs a currency or MPTID + * with its issuer. `PathAsset` carries only the identifier half. Use + * `PathAsset(Asset const&)` to project an `Asset` down to a `PathAsset`. + * + * @see Asset, STPathElement, ValidPathAsset */ class PathAsset { @@ -14,36 +35,83 @@ private: public: PathAsset() = default; - // Enables comparing Asset and PathAsset + + /** Construct a PathAsset by projecting an Asset, discarding the issuer. + * + * For an `Issue`-bearing `Asset`, retains the `Currency`. For an + * `MPTIssue`-bearing `Asset`, retains the `MPTID`. This enables direct + * comparison between the richer `Asset` type and the path-element + * representation without manually extracting the identifier. + * + * @param asset The full asset to project. + */ PathAsset(Asset const& asset); + + /** Construct a PathAsset representing an XRP or IOU currency. */ PathAsset(Currency const& currency) : easset_(currency) { } + + /** Construct a PathAsset representing an MPT issuance. */ PathAsset(MPTID const& mpt) : easset_(mpt) { } + /** Return whether the active alternative is exactly `T`. + * + * @tparam T `Currency` or `MPTID` (enforced by `ValidPathAsset`). + * @return `true` if the held alternative is `T`, `false` otherwise. + */ template [[nodiscard]] constexpr bool holds() const; + /** Return whether this path asset represents native XRP. + * + * A `Currency` alternative delegates to `xrpl::isXRP(currency)`. An + * `MPTID` alternative always returns `false` — MPT can never be native. + * + * @return `true` if the held currency is the XRP zero-currency sentinel. + */ [[nodiscard]] constexpr bool isXRP() const; + /** Return a const reference to the held value of type `T`. + * + * @tparam T `Currency` or `MPTID` (enforced by `ValidPathAsset`). + * @return A reference to the active alternative. + * @throws std::runtime_error if the active alternative is not `T`. Call + * `holds()` or dispatch through `visit()` to avoid this. + */ template T const& get() const; + /** Return a const reference to the underlying variant. + * + * Provides direct access to `std::variant` for callers + * that need to pass it to `std::visit` or store it without going through + * the member `visit()` wrapper. + * + * @return The internal variant holding `Currency` or `MPTID`. + */ [[nodiscard]] constexpr std::variant const& value() const; - // Custom, generic visit implementation + /** Visit the active alternative with a set of per-type callables. + * + * Combines `visitors...` into a single overload set via + * `detail::CombineVisitors` and forwards to `std::visit`. Both + * alternatives (`Currency` and `MPTID`) must be covered. + * + * @tparam Visitors Callable types, one per alternative. + * @param visitors Callables to dispatch to; typically lambdas. + * @return The return value of the selected visitor. + */ template constexpr auto visit(Visitors&&... visitors) const -> decltype(auto) { - // Simple delegation to the reusable utility, passing the internal - // variant data. return detail::visit(easset_, std::forward(visitors)...); } @@ -51,9 +119,23 @@ public: operator==(PathAsset const& lhs, PathAsset const& rhs); }; +/** True when `PA` is `Currency`, false when `PA` is `MPTID`. + * + * Compile-time predicate for `if constexpr` branches in generic code that + * must distinguish XRP/IOU paths from MPT paths. + * + * @tparam PA `Currency` or `MPTID` (enforced by `ValidPathAsset`). + */ template constexpr bool kIS_CURRENCY_V = std::is_same_v; +/** True when `PA` is `MPTID`, false when `PA` is `Currency`. + * + * Compile-time predicate for `if constexpr` branches in generic code that + * must distinguish MPT paths from XRP/IOU paths. + * + * @tparam PA `Currency` or `MPTID` (enforced by `ValidPathAsset`). + */ template constexpr bool kIS_MPTID_V = std::is_same_v; @@ -94,6 +176,17 @@ PathAsset::isXRP() const [](MPTID const&) { return false; }); } +/** Compare two PathAssets for equality. + * + * Two `PathAsset` values are equal only when both hold the same alternative + * type *and* the contained values are equal. A `Currency` and an `MPTID` + * are never equal even if their raw bytes coincide, preventing cross-type + * false positives. + * + * @param lhs Left-hand operand. + * @param rhs Right-hand operand. + * @return `true` if both hold the same type and equal value, `false` otherwise. + */ constexpr bool operator==(PathAsset const& lhs, PathAsset const& rhs) { @@ -112,6 +205,16 @@ operator==(PathAsset const& lhs, PathAsset const& rhs) rhs.value()); } +/** Append a PathAsset's value to a hash state. + * + * Dispatches to the appropriate `hash_append` overload for the active + * alternative (`Currency` or `MPTID`), enabling `PathAsset` to be used as + * a key in hash-based containers built on the `beast::uhash` infrastructure. + * + * @tparam Hasher A type satisfying the `beast::hash_append` Hasher concept. + * @param h The hash accumulator to append to. + * @param pathAsset The path asset whose value is appended. + */ template void hash_append(Hasher& h, PathAsset const& pathAsset) @@ -119,15 +222,41 @@ hash_append(Hasher& h, PathAsset const& pathAsset) std::visit([&](T const& e) { hash_append(h, e); }, pathAsset.value()); } +/** Return whether a PathAsset represents native XRP. + * + * Free-function wrapper for `PathAsset::isXRP()`, provided for symmetry + * with the `isXRP()` overloads for `Currency`, `Asset`, and `STAmount`. + * + * @param asset The path asset to test. + * @return `true` if `asset` holds the XRP zero-currency sentinel. + */ inline bool isXRP(PathAsset const& asset) { return asset.isXRP(); } +/** Produce a human-readable string identifying a PathAsset. + * + * Dispatches to `to_string(Currency const&)` or `to_string(MPTID const&)` + * depending on the active alternative. For a `Currency` this yields the ISO + * 4217 ticker or `"XRP"`; for an `MPTID` it yields the base-58 encoded token + * identifier. + * + * @param asset The path asset to stringify. + * @return A descriptive string identifying the currency or MPT issuance. + */ std::string to_string(PathAsset const& asset); +/** Stream-insert a human-readable description of a PathAsset. + * + * Equivalent to `os << to_string(x)`. Intended for logging and diagnostics. + * + * @param os The output stream to write to. + * @param x The path asset to write. + * @return `os`, for chaining. + */ std::ostream& operator<<(std::ostream& os, PathAsset const& x); diff --git a/include/xrpl/protocol/PayChan.h b/include/xrpl/protocol/PayChan.h index d8f4e0f527..6a93497d37 100644 --- a/include/xrpl/protocol/PayChan.h +++ b/include/xrpl/protocol/PayChan.h @@ -1,3 +1,13 @@ +/** @file + * Canonical serialization for payment channel claim authorizations. + * + * Defines the single function that all three call sites — channel + * authorization (RPC), channel verification (RPC), and on-ledger + * claim validation (transaction engine) — must use to build the + * signed payload. Centralizing this here ensures that a signature + * produced off-ledger is always accepted on-ledger. + */ + #pragma once #include @@ -7,6 +17,34 @@ namespace xrpl { +/** Serialize the signing payload for a payment channel claim authorization. + * + * Writes exactly three fields into @p msg in a protocol-defined order: + * the `HashPrefix::paymentChannelClaim` domain-separation tag (4 bytes), + * the 256-bit channel keylet @p key, and the authorized cumulative amount + * @p amt as a 64-bit drop count. The resulting byte sequence is what the + * channel sender signs and what the recipient or ledger verifies. + * + * This function is the single source of truth for the signed payload layout. + * It is called identically by `channel_authorize` (RPC), `channel_verify` + * (RPC), and `PaymentChannelClaim` preflight (transaction engine). Any drift + * between those sites would cause off-ledger signatures to fail on-ledger + * validation. + * + * @param msg Serializer to append the payload fields into. The caller is + * responsible for constructing the `Serializer` and, after this call, + * passing `msg.slice()` to the sign or verify primitive. + * @param key The 256-bit keylet of the payment channel ledger object. Binds + * the authorization to exactly one channel so it cannot be replayed + * against a different channel. + * @param amt The authorized cumulative ceiling in drops. The on-ledger claim + * validator rejects any claim whose running total exceeds this value. + * + * @note The `HashPrefix::paymentChannelClaim` tag (`'C','L','M',0x00`) is + * protocol-immutable. Changing it would invalidate all existing payment + * channel authorizations. + * @see HashPrefix::paymentChannelClaim + */ inline void serializePayChanAuthorization(Serializer& msg, uint256 const& key, XRPAmount const& amt) { diff --git a/include/xrpl/protocol/Permissions.h b/include/xrpl/protocol/Permissions.h index 5d56fa4461..a54495da8e 100644 --- a/include/xrpl/protocol/Permissions.h +++ b/include/xrpl/protocol/Permissions.h @@ -1,3 +1,17 @@ +/** @file + * Central definition of XRPL's account-delegation permission system, + * used by the `DelegateSet` transaction type. + * + * Two numeric ranges partition the `sfPermissionValue` field stored + * on-ledger: + * - **Transaction-level** (≤ `UINT16_MAX`): `TxType + 1`, granting + * authority over an entire transaction type. + * - **Granular** (> `UINT16_MAX`, minimum 65537): covers a specific + * sub-operation within a transaction type (e.g., freezing a trustline + * without being able to authorize it). + * + * The `Permission` singleton is the runtime authority for both ranges. + */ #pragma once #include @@ -9,12 +23,21 @@ #include namespace xrpl { -/** - * We have both transaction type permissions and granular type permissions. - * Since we will reuse the TransactionFormats to parse the Transaction - * Permissions, only the GranularPermissionType is defined here. To prevent - * conflicts with TxType, the GranularPermissionType is always set to a value - * greater than the maximum value of uint16. + +/** Granular sub-operation permission values used by the delegation system. + * + * Each enumerator targets a specific capability within a parent transaction + * type, enabling fine-grained delegation without granting broad transaction- + * level authority. For example, `TrustlineFreeze` delegates only the ability + * to freeze a trustline via `ttTRUST_SET`, not to authorize or unfreeze. + * + * All values are greater than `UINT16_MAX` (minimum 65537), which keeps them + * numerically disjoint from transaction-level permissions (≤ `UINT16_MAX`). + * This invariant is asserted at startup inside the `Permission` constructor. + * + * Generated from `detail/permissions.macro` via the X-macro pattern. Adding + * a new sub-operation requires only a single `PERMISSION(...)` entry in that + * file. */ // Macro-generated, complex // NOLINTNEXTLINE(cppcoreguidelines-use-enum-class) @@ -30,27 +53,67 @@ enum GranularPermissionType : std::uint32_t { #pragma pop_macro("PERMISSION") }; +/** Indicates whether a transaction type may be delegated in bulk via + * a transaction-level `DelegateSet` permission. + * + * The policy for each `TxType` is encoded in `detail/transactions.macro` + * as the `delegable` parameter of every `TRANSACTION(...)` entry. + * Sensitive types such as `ttACCOUNT_SET` and `ttREGULAR_KEY_SET` are + * `NotDelegable`; most operational types are `Delegable`. + * + * @note Bare enumerators (`xrpl::Delegable` / `xrpl::NotDelegable`) are + * required by preprocessor expansions in tests and macro-generated + * code; `enum class` would break that usage. + */ // Injected bare enumerators (xrpl::delegable / xrpl::notDelegable) are required by preprocessor // tricks in tests and macro-generated code; enum class would break that. // NOLINTNEXTLINE(cppcoreguidelines-use-enum-class) enum Delegation { Delegable, NotDelegable }; +/** Central authority for XRPL's account-delegation permission system. + * + * A Meyer's singleton populated at first call to `getInstance()`. Its + * constructor expands `transactions.macro` and `permissions.macro` to build + * five immutable lookup maps covering every known transaction type and + * granular sub-operation. After construction the maps are never mutated, + * so all concurrent read access from transaction-processing threads is safe + * without synchronization. + * + * The two principal call sites are: + * - `DelegateSet::preflight()` — calls `isDelegable()` to validate each + * `sfPermissionValue` before it is written on-ledger. + * - `DelegateUtils` / transactors — call `getGranularTxType()` and related + * helpers to enforce granular limits at execution time. + */ class Permission { private: Permission(); + /** Maps each `TxType` to the amendment required to use it, or `uint256{}` if none. */ std::unordered_map txFeatureMap_; + /** Maps each `TxType` to its `Delegable` / `NotDelegable` policy tag. */ std::unordered_map delegableTx_; + /** Maps granular permission name strings to their `GranularPermissionType` values. */ std::unordered_map granularPermissionMap_; + /** Maps `GranularPermissionType` values to their name strings (inverse of `granularPermissionMap_`). */ std::unordered_map granularNameMap_; + /** Maps each `GranularPermissionType` to its parent `TxType`. */ std::unordered_map granularTxTypeMap_; public: + /** Returns the process-wide singleton instance. + * + * Initialized on first call via a function-local `static`; C++11 + * guarantees thread-safe initialization. The instance is never mutated + * after construction. + * + * @return A `const` reference to the singleton `Permission` object. + */ static Permission const& getInstance(); @@ -58,29 +121,125 @@ public: Permission& operator=(Permission const&) = delete; + /** Resolves a raw `sfPermissionValue` to its human-readable name. + * + * Checks the granular permission table first (values > `UINT16_MAX`). + * If unrecognized there, decodes the value as a transaction-level + * permission (`value - 1` = `TxType`) and delegates to `TxFormats` for + * the canonical name. Used by `STUInt32::getText()` and + * `STUInt32::getJson()` to render any `sfPermissionValue` as a string + * instead of a raw number. + * + * @param value Raw `sfPermissionValue` from the ledger. + * @return The permission name, or `std::nullopt` if `value` is not + * recognized as either a granular or transaction-level permission. + */ [[nodiscard]] std::optional getPermissionName(std::uint32_t const value) const; + /** Looks up the numeric wire value of a granular permission by name. + * + * Used when deserializing `sfPermissionValue` from JSON (e.g., during + * `DelegateSet` preflight or RPC input parsing) to convert a + * human-readable name like `"TrustlineFreeze"` back to its `uint32_t` + * representation. + * + * @param name Case-sensitive granular permission name. + * @return The corresponding `uint32_t` wire value, or `std::nullopt` if + * `name` is not a known granular permission. + */ [[nodiscard]] std::optional getGranularValue(std::string const& name) const; + /** Looks up the name of a granular permission by its enum value. + * + * Inverse of `getGranularValue`; used when serializing a granular + * permission value to human-readable output. + * + * @param value A `GranularPermissionType` enum value. + * @return The permission name string, or `std::nullopt` if `value` is + * not a known granular permission. + */ [[nodiscard]] std::optional getGranularName(GranularPermissionType const& value) const; + /** Returns the parent transaction type for a granular permission. + * + * Multiple granular permissions share the same parent `TxType`; for + * example, `TrustlineAuthorize`, `TrustlineFreeze`, and + * `TrustlineUnfreeze` all map to `ttTRUST_SET`. Used by `isDelegable()` + * and execution-time helpers to locate the relevant transactor context + * and required amendment for a granular sub-operation. + * + * @param gpType A `GranularPermissionType` enum value. + * @return The parent `TxType`, or `std::nullopt` if `gpType` is not a + * known granular permission. + */ [[nodiscard]] std::optional getGranularTxType(GranularPermissionType const& gpType) const; + /** Returns the amendment required to use a transaction type, if any. + * + * A `uint256{}` stored in `txFeatureMap_` means the transaction type + * requires no enabling amendment. In that case `std::nullopt` is + * returned, signalling that the type is unconditionally available. + * + * @param txType A recognized transaction type. + * @return A const reference to the required amendment hash wrapped in + * `std::optional`, or `std::nullopt` if no amendment is required. + * @note Asserts in debug builds that `txType` is present in + * `txFeatureMap_`. Passing an unregistered `TxType` is a + * programming error (a transaction missing from `transactions.macro`). + */ [[nodiscard]] std::optional> getTxFeature(TxType txType) const; + /** Determines whether a permission value may appear in a `DelegateSet` + * transaction under the current ledger rules. + * + * The check differs by permission kind: + * - **Granular** (value > `UINT16_MAX`): accepted whenever the value + * resolves to a known `GranularPermissionType`; no further gate is + * applied because granular permissions are inherently narrow. + * - **Transaction-level** (value ≤ `UINT16_MAX`): accepted only when the + * decoded `TxType` is recognized, its required amendment is currently + * enabled in `rules` (or no amendment is required), and the type is + * marked `Delegable` in `transactions.macro`. + * + * @param permissionValue Raw `sfPermissionValue` to validate. + * @param rules Active amendment rules for the current ledger. + * @return `true` if the permission may be granted, `false` otherwise. + * @note The amendment check prevents a transaction type from being + * delegated before the ledger feature that introduces it is live, + * even if the macro table already includes it. + */ [[nodiscard]] bool isDelegable(std::uint32_t const& permissionValue, Rules const& rules) const; - // for tx level permission, permission value is equal to tx type plus one + /** Converts a `TxType` to its transaction-level permission value. + * + * Transaction-level permissions are encoded as `TxType + 1`. The `+1` + * offset ensures zero is never a valid permission value and keeps the + * entire range within `uint16` (transaction-level permissions ≤ + * `UINT16_MAX`). + * + * @param type A transaction type. + * @return The corresponding `uint32_t` permission value (`TxType + 1`). + * @see permissionToTxType + */ static uint32_t txToPermissionType(TxType const& type); - // tx type value is permission value minus one + /** Converts a transaction-level permission value back to its `TxType`. + * + * Inverse of `txToPermissionType`. Callers must verify that `value` is + * in the transaction-level range (≤ `UINT16_MAX`) before calling; this + * function performs no range check. + * + * @param value A transaction-level permission value (`TxType + 1`). + * @return The decoded `TxType` (`value - 1`). + * @see txToPermissionType + */ static TxType permissionToTxType(uint32_t const& value); }; diff --git a/include/xrpl/protocol/Protocol.h b/include/xrpl/protocol/Protocol.h index 50b2425016..2e07a09e33 100644 --- a/include/xrpl/protocol/Protocol.h +++ b/include/xrpl/protocol/Protocol.h @@ -1,3 +1,16 @@ +/** @file + * Canonical source of XRPL protocol constants and boundary predicates. + * + * Every hard-coded numeric limit that, if changed silently, would cause a + * **hard fork** — a ledger-state disagreement between nodes running different + * software versions — is defined here. All constants are `constexpr` and + * therefore available at compile time with zero runtime overhead. + * + * @note Changing any value in this file without pairing the change with an + * amendment-gated detection mechanism will split the network. + * + * @ingroup protocol + */ #pragma once #include @@ -8,100 +21,182 @@ namespace xrpl { -/** Protocol specific constants. - - This information is, implicitly, part of the protocol. - - @note Changing these values without adding code to the - server to detect "pre-change" and "post-change" - will result in a hard fork. - - @ingroup protocol -*/ -/** Smallest legal byte size of a transaction. */ +/** Smallest legal serialized size of a transaction, in bytes. + * + * Transactions below this threshold are trivially malformed and are rejected + * before deserialization begins. + */ std::size_t constexpr kTX_MIN_SIZE_BYTES = 32; -/** Largest legal byte size of a transaction. */ +/** Largest legal serialized size of a transaction, in bytes. + * + * The 1 MB cap protects node memory and network bandwidth. Transactions + * exceeding this limit are rejected on receipt without further processing. + */ std::size_t constexpr kTX_MAX_SIZE_BYTES = megabytes(1); -/** The maximum number of unfunded offers to delete at once */ +/** Maximum number of unfunded offers that may be removed in a single + * transaction pass. + * + * Unfunded-offer cleanup is opportunistic: stale offers are removed as a + * side-effect of offer placement. Capping the count keeps the worst-case + * execution time of a single transaction predictable. + * + * @note The asymmetry with `kEXPIRED_OFFER_REMOVE_LIMIT` (1000 vs 256) + * reflects that unfunded-offer removal was designed to handle larger + * batches; expired offers are discovered through a different, narrower + * path. + */ std::size_t constexpr kUNFUNDED_OFFER_REMOVE_LIMIT = 1000; -/** The maximum number of expired offers to delete at once */ +/** Maximum number of expired offers that may be removed in a single + * transaction pass. + * + * @see kUNFUNDED_OFFER_REMOVE_LIMIT for the rationale behind the asymmetric + * cap. + */ std::size_t constexpr kEXPIRED_OFFER_REMOVE_LIMIT = 256; -/** The maximum number of metadata entries allowed in one transaction */ +/** Maximum number of metadata entries a single transaction may produce. + * + * When a transaction would exceed this cap the transactor returns + * `tecOVERSIZE`, triggering a controlled teardown that applies the fee + * and rolls back ledger mutations rather than allowing unbounded metadata + * growth. + */ std::size_t constexpr kOVERSIZE_META_DATA_CAP = 5200; -/** The maximum number of entries per directory page */ +/** Maximum number of entries per owner-directory or offer-directory page. + * + * Keeping pages small bounds the work required to traverse a directory: + * each page hop visits at most 32 entries. + */ std::size_t constexpr kDIR_NODE_MAX_ENTRIES = 32; -/** The maximum number of pages allowed in a directory - - Made obsolete by fixDirectoryLimit amendment. -*/ +/** Historical maximum number of pages in a single directory. + * + * This limit was enforced before the `fixDirectoryLimit` amendment. + * Post-amendment, directories may grow beyond 262 144 pages; this + * constant is retained for pre-amendment replay correctness. + * + * @note Pre-amendment code returns `tecDIR_FULL` when this limit is + * reached. Post-amendment, only unsigned-integer overflow can + * produce a null page index. + */ std::uint64_t constexpr kDIR_NODE_MAX_PAGES = 262144; -/** The maximum number of items in an NFT page */ +/** Maximum number of NFToken entries per NFT directory page. */ std::size_t constexpr kDIR_MAX_TOKENS_PER_PAGE = 32; -/** The maximum number of owner directory entries for account to be deletable */ +/** Maximum number of owner-directory entries an account may hold and still + * be eligible for deletion via `AccountDelete`. + * + * Accounts with more than 1000 directory entries cannot be deleted; this + * protects against unbounded cleanup work within a single transaction. + */ std::size_t constexpr kMAX_DELETABLE_DIR_ENTRIES = 1000; -/** The maximum number of token offers that can be canceled at once */ +/** Maximum number of NFToken offers that may be cancelled in a single + * `NFTokenCancelOffer` transaction. + */ std::size_t constexpr kMAX_TOKEN_OFFER_CANCEL_COUNT = 500; -/** The maximum number of offers in an offer directory for NFT to be burnable */ +/** Maximum number of NFToken offers that must be cleaned up before an NFT + * can be burned. + * + * An NFT with more than 500 live offers cannot be burned until the excess + * offers are cancelled first. + */ std::size_t constexpr kMAX_DELETABLE_TOKEN_OFFER_ENTRIES = 500; -/** The maximum token transfer fee allowed. - - Token transfer fees can range from 0% to 50% and are specified in tenths of - a basis point; that is a value of 1000 represents a transfer fee of 1% and - a value of 10000 represents a transfer fee of 10%. - - Note that for extremely low transfer fees values, it is possible that the - calculated fee will be 0. +/** Maximum NFToken transfer fee, expressed in tenths of a basis point. + * + * Transfer fees range from 0% to 50%. A value of 1 000 represents 1% and + * a value of 50 000 represents 50%. For very low fee values the computed + * fee amount may round down to zero drops. */ std::uint16_t constexpr kMAX_TRANSFER_FEE = 50000; -/** There are 10,000 basis points (bips) in 100%. +/** Number of basis points (bips) in 100% (unity). * - * Basis points represent 0.01%. + * One basis point equals 0.01%. To compute the share of a value `X` + * corresponding to `B` bips, use `X * B / kBIPS_PER_UNITY`. To convert + * a whole-percentage `P` to bips, use `P * kBIPS_PER_UNITY / 100` + * (or simply call `percentageToBips(P)`). * - * Given a value X, to find the amount for B bps, - * use X * B / bipsPerUnity + * Example: 10% coverage on 999 XRP (999 000 000 drops) = + * `999'000'000 * 1'000 / 10'000` = 99 900 000 drops. * - * Example: If a loan broker has 999 XRP of debt, and must maintain 1,000 bps of - * that debt as cover (10%), then the minimum cover amount is 999,000,000 drops - * * 1000 / bipsPerUnity = 99,900,00 drops or 99.9 XRP. - * - * Given a percentage P, to find the number of bps that percentage represents, - * use P * bipsPerUnity. - * - * Example: 50% is 0.50 * bipsPerUnity = 5,000 bps. + * All ledger fee and rate arithmetic uses integer bips to guarantee + * bit-identical results across all validator platforms. */ Bips32 constexpr kBIPS_PER_UNITY(100 * 100); static_assert(kBIPS_PER_UNITY == Bips32{10'000}); + +/** Number of tenth-basis-points in 100% (unity). + * + * One tenth-basis-point equals 0.001%. Use `percentageToTenthBips(P)` + * to convert a whole percentage, or `tenthBipsOfValue(value, rate)` to + * apply a rate to a value. + */ TenthBips32 constexpr kTENTH_BIPS_PER_UNITY(kBIPS_PER_UNITY.value() * 10); static_assert(kTENTH_BIPS_PER_UNITY == TenthBips32(100'000)); +/** Convert a whole-percentage value to a strongly-typed `Bips32`. + * + * Uses integer division; fractional basis points are truncated. + * + * @param percentage An integer percentage in [0, 100]. + * @return The equivalent number of basis points as a `Bips32`. + */ constexpr Bips32 percentageToBips(std::uint32_t percentage) { return Bips32(percentage * kBIPS_PER_UNITY.value() / 100); } + +/** Convert a whole-percentage value to a strongly-typed `TenthBips32`. + * + * Uses integer division; fractional tenth-bips are truncated. + * + * @param percentage An integer percentage in [0, 100]. + * @return The equivalent number of tenth-basis-points as a `TenthBips32`. + */ constexpr TenthBips32 percentageToTenthBips(std::uint32_t percentage) { return TenthBips32(percentage * kTENTH_BIPS_PER_UNITY.value() / 100); } + +/** Compute the basis-point share of a value using integer arithmetic. + * + * Calculates `value * bips / kBIPS_PER_UNITY` without floating point, + * guaranteeing deterministic results on all platforms. + * + * @tparam T Numeric type of the value (must support `*` and `/`). + * @tparam TBips Underlying storage type of the `Bips` wrapper. + * @param value The base amount to take a share of. + * @param bips The rate in basis points. + * @return The share of `value` at the given rate, truncated toward zero. + */ template constexpr T bipsOfValue(T value, Bips bips) { return value * bips.value() / kBIPS_PER_UNITY.value(); } + +/** Compute the tenth-basis-point share of a value using integer arithmetic. + * + * Calculates `value * bips / kTENTH_BIPS_PER_UNITY` without floating + * point, guaranteeing deterministic results on all platforms. + * + * @tparam T Numeric type of the value (must support `*` and `/`). + * @tparam TBips Underlying storage type of the `TenthBips` wrapper. + * @param value The base amount to take a share of. + * @param bips The rate in tenth-basis-points. + * @return The share of `value` at the given rate, truncated toward zero. + */ template constexpr T tenthBipsOfValue(T value, TenthBips bips) @@ -109,202 +204,293 @@ tenthBipsOfValue(T value, TenthBips bips) return value * bips.value() / kTENTH_BIPS_PER_UNITY.value(); } +/** Rate and limit constants specific to the on-ledger lending protocol. */ namespace Lending { -/** The maximum management fee rate allowed by a loan broker in 1/10 bips. - Valid values are between 0 and 10% inclusive. -*/ +/** Maximum management fee a LoanBroker may charge, in tenth-basis-points. + * + * Valid values are in [0, 10%]. Stored as `TenthBips16` (fits in + * `uint16_t`) because 10 000 < 65 535. + */ TenthBips16 constexpr kMAX_MANAGEMENT_FEE_RATE( unsafeCast(percentageToTenthBips(10).value())); static_assert(kMAX_MANAGEMENT_FEE_RATE == TenthBips16(std::uint16_t(10'000u))); -/** The maximum coverage rate required of a loan broker in 1/10 bips. - - Valid values are between 0 and 100% inclusive. -*/ +/** Maximum coverage rate a LoanBroker must maintain, in tenth-basis-points. + * + * The coverage rate specifies the minimum fraction of outstanding loan + * debt that the broker must hold as collateral. Valid values are in + * [0, 100%]. + */ TenthBips32 constexpr kMAX_COVER_RATE = percentageToTenthBips(100); static_assert(kMAX_COVER_RATE == TenthBips32(100'000u)); -/** The maximum overpayment fee on a loan in 1/10 bips. -* - Valid values are between 0 and 100% inclusive. -*/ +/** Maximum overpayment fee on a loan, in tenth-basis-points. + * + * Applied when a borrower pays more than the scheduled amount. Valid + * values are in [0, 100%]. + */ TenthBips32 constexpr kMAX_OVERPAYMENT_FEE = percentageToTenthBips(100); static_assert(kMAX_OVERPAYMENT_FEE == TenthBips32(100'000u)); -/** Annualized interest rate of the Loan in 1/10 bips. +/** Maximum annualized interest rate on a Loan, in tenth-basis-points. * - * Valid values are between 0 and 100% inclusive. + * Valid values are in [0, 100%]. */ TenthBips32 constexpr kMAX_INTEREST_RATE = percentageToTenthBips(100); static_assert(kMAX_INTEREST_RATE == TenthBips32(100'000u)); -/** The maximum premium added to the interest rate for late payments on a loan - * in 1/10 bips. +/** Maximum late-payment interest premium on a Loan, in tenth-basis-points. * - * Valid values are between 0 and 100% inclusive. + * This rate is added to the base interest rate when payments are overdue. + * Valid values are in [0, 100%]. */ TenthBips32 constexpr kMAX_LATE_INTEREST_RATE = percentageToTenthBips(100); static_assert(kMAX_LATE_INTEREST_RATE == TenthBips32(100'000u)); -/** The maximum close interest rate charged for repaying a loan early in 1/10 - * bips. +/** Maximum early-repayment (close) interest rate on a Loan, in + * tenth-basis-points. * - * Valid values are between 0 and 100% inclusive. + * Charged when a borrower repays a loan ahead of schedule. Valid values + * are in [0, 100%]. */ TenthBips32 constexpr kMAX_CLOSE_INTEREST_RATE = percentageToTenthBips(100); static_assert(kMAX_CLOSE_INTEREST_RATE == TenthBips32(100'000u)); -/** The maximum overpayment interest rate charged on loan overpayments in 1/10 - * bips. +/** Maximum overpayment interest rate charged on loan overpayments, in + * tenth-basis-points. * - * Valid values are between 0 and 100% inclusive. + * Valid values are in [0, 100%]. */ TenthBips32 constexpr kMAX_OVERPAYMENT_INTEREST_RATE = percentageToTenthBips(100); static_assert(kMAX_OVERPAYMENT_INTEREST_RATE == TenthBips32(100'000u)); -/** LoanPay transaction cost will be one base fee per X combined payments +/** Number of loan payments per base-fee increment charged by `LoanPay`. * - * The number of payments is estimated based on the Amount paid and the Loan's - * Fixed Payment size. Overpayments (indicated with the tfLoanOverpayment flag) - * count as one more payment. + * The fee is estimated from the transaction `Amount` divided by the + * loan's fixed payment size. Overpayments (flagged with + * `tfLoanOverpayment`) count as one additional payment in the estimate. + * One base fee unit is charged for every 5 estimated payments. * - * This number was chosen arbitrarily, but should not be changed once released - * without an amendment + * @note This value was chosen arbitrarily and is amendment-locked once + * released: changing it without an amendment would alter the fee + * schedule for existing `LoanPay` transactions. + * @see kLOAN_MAXIMUM_PAYMENTS_PER_TRANSACTION */ static constexpr int kLOAN_PAYMENTS_PER_FEE_INCREMENT = 5; -/** Maximum number of combined payments that a LoanPay transaction will process +/** Hard cap on the number of combined payments processed by one `LoanPay`. * - * This limit is enforced during the loan payment process, and thus is not - * estimated. If the limit is hit, no further payments or overpayments will be - * processed, no matter how much of the transaction Amount is left, but the - * transaction will succeed with the payments that have been processed up to - * that point. + * This limit is enforced during execution, not during fee estimation. + * When the cap is reached the transaction succeeds with the payments + * processed so far; any remaining `Amount` is not applied. * - * This limit is independent of loanPaymentsPerFeeIncrement, so a transaction - * could potentially be charged for many more payments than actually get - * processed. Users should take care not to submit a transaction paying more - * than loanMaximumPaymentsPerTransaction * Loan.PeriodicPayment. Because - * overpayments are charged as a payment, if submitting - * loanMaximumPaymentsPerTransaction * Loan.PeriodicPayment, users should not - * set the tfLoanOverpayment flag. + * Because the fee is based on the *estimated* payment count (derived from + * `Amount / PeriodicPayment`) and the cap is enforced on the *actual* + * count, a transaction can be charged for more payments than it processes. + * Submitters should not exceed + * `kLOAN_MAXIMUM_PAYMENTS_PER_TRANSACTION * Loan.PeriodicPayment` in + * `Amount`, and should omit `tfLoanOverpayment` if paying exactly that + * much. * - * Even though they're independent, loanMaximumPaymentsPerTransaction should be - * a multiple of loanPaymentsPerFeeIncrement. - * - * This number was chosen arbitrarily, but should not be changed once released - * without an amendment + * @note `kLOAN_MAXIMUM_PAYMENTS_PER_TRANSACTION` must remain a multiple + * of `kLOAN_PAYMENTS_PER_FEE_INCREMENT`; this invariant is checked + * at startup via `static_assert` in LoanPay.cpp. Both values are + * amendment-locked once released. */ static constexpr int kLOAN_MAXIMUM_PAYMENTS_PER_TRANSACTION = 100; } // namespace Lending -/** The maximum length of a URI inside an NFT */ +/** Maximum byte length of a URI stored in an NFToken. */ std::size_t constexpr kMAX_TOKEN_URI_LENGTH = 256; -/** The maximum length of a Data element inside a DID */ +/** Maximum byte length of the `Data` field (DID document) in a DID object. */ std::size_t constexpr kMAX_DID_DOCUMENT_LENGTH = 256; -/** The maximum length of a URI inside a DID */ +/** Maximum byte length of the `URI` field in a DID object. */ std::size_t constexpr kMAX_DIDURI_LENGTH = 256; -/** The maximum length of an Attestation inside a DID */ +/** Maximum byte length of the `Attestation` field in a DID object. */ std::size_t constexpr kMAX_DID_DATA_LENGTH = 256; -/** The maximum length of a domain */ +/** Maximum byte length of an account `Domain` field. */ std::size_t constexpr kMAX_DOMAIN_LENGTH = 256; -/** The maximum length of a URI inside a Credential */ +/** Maximum byte length of the `URI` field in a Credential object. */ std::size_t constexpr kMAX_CREDENTIAL_URI_LENGTH = 256; -/** The maximum length of a CredentialType inside a Credential */ +/** Maximum byte length of the `CredentialType` field in a Credential object. + * + * Narrower than the 256-byte default to keep credential-type strings + * human-readable and prevent abuse of the type field as an arbitrary blob. + */ std::size_t constexpr kMAX_CREDENTIAL_TYPE_LENGTH = 64; -/** The maximum number of credentials can be passed in array */ +/** Maximum number of credentials that may appear in a transaction's + * `Credentials` array. + */ std::size_t constexpr kMAX_CREDENTIALS_ARRAY_SIZE = 8; -/** The maximum number of credentials can be passed in array for permissioned - * domain */ +/** Maximum number of credentials that a permissioned domain may reference. */ std::size_t constexpr kMAX_PERMISSIONED_DOMAIN_CREDENTIALS_ARRAY_SIZE = 10; -/** The maximum length of MPTokenMetadata */ +/** Maximum byte length of the `MPTokenMetadata` field on an MPTokenIssuance. */ std::size_t constexpr kMAX_MP_TOKEN_METADATA_LENGTH = 1024; -/** The maximum amount of MPTokenIssuance */ +/** Maximum quantity representable by an MPToken amount field. + * + * Equal to `INT64_MAX` (2^63 − 1). The `static_assert` below guarantees + * that the XRPL `Number` type can represent every valid MPToken quantity + * without overflow. + */ std::uint64_t constexpr kMAX_MP_TOKEN_AMOUNT = 0x7FFF'FFFF'FFFF'FFFFull; static_assert(Number::kMAX_REP >= kMAX_MP_TOKEN_AMOUNT); -/** The maximum length of Data payload */ +/** Maximum byte length of the `Data` payload field. */ std::size_t constexpr kMAX_DATA_PAYLOAD_LENGTH = 256; -/** Vault withdrawal policies */ +/** Vault withdrawal policy: first-come, first-served. + * + * The numeric value 1 is the wire-stable identifier for this strategy; + * it must not change once released. + */ std::uint8_t constexpr kVAULT_STRATEGY_FIRST_COME_FIRST_SERVE = 1; -/** Default IOU scale factor for a Vault */ +/** Default IOU-to-share scale exponent for a Vault. + * + * When no explicit scale is specified at Vault creation the scale + * defaults to 6, meaning one IOU unit maps to 10^6 shares. This + * applies only to IOU-backed vaults; native-asset and MPT vaults always + * use scale 0. + */ std::uint8_t constexpr kVAULT_DEFAULT_IOU_SCALE = 6; -/** Maximum scale factor for a Vault. The number is chosen to ensure that -1 IOU can be always converted to shares. -10^19 > maxMPTokenAmount (2^64-1) > 10^18 */ + +/** Maximum IOU-to-share scale exponent for a Vault. + * + * Chosen so that exactly one IOU unit can always be converted to at + * least one share: 10^19 > `kMAX_MP_TOKEN_AMOUNT` (≈ 2^63) > 10^18. + * Preflight rejects any `VaultCreate` that specifies a scale above this + * value with `temMALFORMED`. Applies only to IOU-backed vaults. + */ std::uint8_t constexpr kVAULT_MAXIMUM_IOU_SCALE = 18; -/** Maximum recursion depth for vault shares being put as an asset inside - * another vault; counted from 0 */ +/** Maximum recursion depth when checking whether a vault's asset is itself + * backed by another vault. + * + * Counted from 0, so a depth of 5 permits at most 6 levels of nesting. + * This prevents pathological chains from consuming unbounded stack space + * during asset-validation traversal. + */ std::uint8_t constexpr kMAX_ASSET_CHECK_DEPTH = 5; -/** A ledger index. */ +/** Ledger sequence number type. + * + * A named alias for `uint32_t` that makes function signatures + * self-documenting wherever ledger positions are passed. + */ using LedgerIndex = std::uint32_t; +/** Number of ledgers between consecutive flag-ledger boundaries. + * + * Every 256 ledgers the network applies accumulated validator votes for + * fee adjustments, reserve requirements, amendment activation, and + * Negative UNL reliability scoring. Both `isFlagLedger()` and + * `isVotingLedger()` test `seq % kFLAG_LEDGER_INTERVAL == 0`; the + * semantic distinction between the two predicates is resolved by callers + * via a `+1` offset on the sequence number they pass. + * + * @note This constant is an implicit part of the wire protocol. Changing + * it without an amendment-gated migration path will cause a hard fork. + */ std::uint32_t constexpr kFLAG_LEDGER_INTERVAL = 256; -/** Returns true if the given ledgerIndex is a voting ledgerIndex */ +/** Return `true` if @p seq is a voting ledger. + * + * Semantically, this asks: "will the ledger built *on top of* `seq` + * be a flag ledger?" Callers therefore pass `seq + 1` (the sequence of + * the ledger currently being assembled). `RCLConsensus` uses this + * predicate to decide whether to inject Negative UNL pseudo-transactions + * for the new consensus round. + * + * The arithmetic is identical to `isFlagLedger`; the two names exist to + * make the `+1` offset explicit at each call site without embedding it + * inside these functions. + * + * @param seq The ledger index to test (typically the previous ledger's + * sequence plus one). + * @return `true` if `seq % kFLAG_LEDGER_INTERVAL == 0`. + * @see isFlagLedger + */ bool isVotingLedger(LedgerIndex seq); -/** Returns true if the given ledgerIndex is a flag ledgerIndex */ +/** Return `true` if @p seq is a flag ledger. + * + * A flag ledger is any ledger whose sequence number is an exact multiple + * of `kFLAG_LEDGER_INTERVAL` (256). It is the ledger in which fee-vote + * and amendment pseudo-transactions are applied, and in which Negative + * UNL reliability updates take effect. `Change::doApply` and + * `FeeVoteImpl` gate their parameter-update logic on this predicate. + * + * Callers pass the ledger's **own** sequence number to ask "has this + * ledger already crossed the boundary?", as opposed to `isVotingLedger`, + * which is called with `seq + 1`. + * + * @param seq The ledger index to test. + * @return `true` if `seq % kFLAG_LEDGER_INTERVAL == 0`. + * @see isVotingLedger + */ bool isFlagLedger(LedgerIndex seq); -/** A transaction identifier. - The value is computed as the hash of the - canonicalized, serialized transaction object. -*/ +/** Transaction identifier type. + * + * A 256-bit hash computed over the canonicalized, serialized transaction + * object using `HashPrefix::transactionID` as the domain separator. + */ using TxID = uint256; -/** The maximum number of trustlines to delete as part of AMM account - * deletion cleanup. +/** Maximum number of AMM trust lines that may be deleted as part of an + * AMM account-deletion cleanup pass. */ std::uint16_t constexpr kMAX_DELETABLE_AMM_TRUST_LINES = 512; -/** The maximum length of a URI inside an Oracle */ +/** Maximum byte length of the `URI` field in an Oracle object. */ std::size_t constexpr kMAX_ORACLE_URI = 256; -/** The maximum length of a Provider inside an Oracle */ +/** Maximum byte length of the `Provider` field in an Oracle object. */ std::size_t constexpr kMAX_ORACLE_PROVIDER = 256; -/** The maximum size of a data series array inside an Oracle */ +/** Maximum number of price data-series entries in an Oracle object. */ std::size_t constexpr kMAX_ORACLE_DATA_SERIES = 10; -/** The maximum length of a SymbolClass inside an Oracle */ +/** Maximum byte length of the `SymbolClass` field in an Oracle object. */ std::size_t constexpr kMAX_ORACLE_SYMBOL_CLASS = 16; -/** The maximum allowed time difference between lastUpdateTime and the time - of the last closed ledger -*/ +/** Maximum allowed age of an Oracle price update, in seconds. + * + * `OracleSet` rejects updates whose `LastUpdateTime` differs from the + * last-closed-ledger close time by more than 300 seconds (5 minutes). + */ std::size_t constexpr kMAX_LAST_UPDATE_TIME_DELTA = 300; -/** The maximum price scaling factor - */ +/** Maximum price-scaling exponent accepted in an Oracle object. */ std::size_t constexpr kMAX_PRICE_SCALE = 20; -/** The maximum percentage of outliers to trim +/** Maximum percentage of outlier data points to trim in Oracle price + * aggregation. */ std::size_t constexpr kMAX_TRIM = 25; -/** The maximum number of delegate permissions an account can grant - */ +/** Maximum number of granular delegate permissions an account may grant. */ std::size_t constexpr kPERMISSION_MAX_SIZE = 10; -/** The maximum number of transactions that can be in a batch. */ +/** Maximum number of inner transactions in a single Batch transaction. + * + * Enforced during preflight; batches exceeding this count are rejected. + * The limit directly bounds the worst-case compute cost for batch + * signature validation and fee calculation. + */ std::size_t constexpr kMAX_BATCH_TX_COUNT = 8; } // namespace xrpl diff --git a/include/xrpl/protocol/PublicKey.h b/include/xrpl/protocol/PublicKey.h index 16d558d73c..353cefcc54 100644 --- a/include/xrpl/protocol/PublicKey.h +++ b/include/xrpl/protocol/PublicKey.h @@ -16,35 +16,31 @@ namespace xrpl { -/** A public key. - - Public keys are used in the public-key cryptography - system used to verify signatures attached to messages. - - The format of the public key is XRPL specific, - information needed to determine the cryptosystem - parameters used is stored inside the key. - - As of this writing two systems are supported: - - secp256k1 - ed25519 - - secp256k1 public keys consist of a 33 byte - compressed public key, with the lead byte equal - to 0x02 or 0x03. - - The ed25519 public keys consist of a 1 byte - prefix constant 0xED, followed by 32 bytes of - public key data. -*/ +/** Immutable 33-byte value type holding an XRPL public key. + * + * Supports both secp256k1 and Ed25519 cryptosystems. The lead byte acts as + * a self-describing type tag — `0x02`/`0x03` for secp256k1 compressed keys, + * `0xED` for Ed25519 keys (an XRPL-specific prefix that pads the native + * 32-byte Ed25519 key to the common 33-byte size). This uniform encoding + * allows `publicKeyType()` to identify the algorithm in O(1) from the raw + * bytes alone, with no external metadata. + * + * The default constructor is deleted; the only construction path is from a + * `Slice`. If the slice does not represent a recognized key format, + * construction calls `LogicError` (process termination) rather than + * throwing — an invalid key at this point indicates a programming error, + * not a recoverable runtime condition. Any live `PublicKey` object is + * therefore always well-formed and algorithm-identified. + * + * The implicit conversion to `Slice` is intentional: it lets `PublicKey` + * flow into serialization and hashing APIs without explicit casting. + */ class PublicKey { protected: - // All the constructed public keys are valid, non-empty and contain 33 - // bytes of data. + /** Uniform storage size in bytes for all supported key types. */ static constexpr std::size_t kSIZE = 33; - std::uint8_t buf_[kSIZE]{}; // should be large enough + std::uint8_t buf_[kSIZE]{}; public: using const_iterator = std::uint8_t const*; @@ -56,72 +52,89 @@ public: PublicKey& operator=(PublicKey const& other); - /** Create a public key. - - Preconditions: - publicKeyType(slice) != std::nullopt - */ + /** Construct from a raw byte slice. + * + * Copies exactly 33 bytes from `slice` after verifying that the bytes + * represent a recognized key format (secp256k1 or Ed25519). Calls + * `LogicError` — terminating the process — if the slice is undersized + * or does not pass `publicKeyType()`. + * + * @param slice Raw bytes to construct from; must satisfy + * `publicKeyType(slice) != std::nullopt`. + * @note Use `publicKeyType()` to validate untrusted input before + * constructing; `parseBase58` does this automatically + * for Base58-encoded keys. + */ explicit PublicKey(Slice const& slice); + /** Return a pointer to the raw 33-byte key buffer. */ [[nodiscard]] std::uint8_t const* data() const noexcept { return buf_; } + /** Return the fixed size of all `PublicKey` objects (always 33). */ static std::size_t size() noexcept { return kSIZE; } + /** Return an iterator to the first byte of the key buffer. */ [[nodiscard]] const_iterator begin() const noexcept { return buf_; } + /** Return a const iterator to the first byte of the key buffer. */ [[nodiscard]] const_iterator cbegin() const noexcept { return buf_; } + /** Return an iterator past the last byte of the key buffer. */ [[nodiscard]] const_iterator end() const noexcept { return buf_ + kSIZE; } + /** Return a const iterator past the last byte of the key buffer. */ [[nodiscard]] const_iterator cend() const noexcept { return buf_ + kSIZE; } + /** Return a `Slice` view over the 33-byte key buffer. */ [[nodiscard]] Slice slice() const noexcept { return {buf_, kSIZE}; } + /** Implicit conversion to `Slice` for use with serialization APIs. */ operator Slice() const noexcept { return slice(); } }; -/** Print the public key to a stream. - */ +/** Write the public key as a hex string to a stream. */ std::ostream& operator<<(std::ostream& os, PublicKey const& pk); +/** Return `true` if both keys hold identical 33-byte representations. */ inline bool operator==(PublicKey const& lhs, PublicKey const& rhs) { return std::memcmp(lhs.data(), rhs.data(), rhs.size()) == 0; } +/** Return `true` if `lhs` is lexicographically less than `rhs`. */ inline bool operator<(PublicKey const& lhs, PublicKey const& rhs) { @@ -129,6 +142,15 @@ operator<(PublicKey const& lhs, PublicKey const& rhs) lhs.data(), lhs.data() + lhs.size(), rhs.data(), rhs.data() + rhs.size()); } +/** Feed the raw 33-byte key into a hash algorithm. + * + * Enables `PublicKey` to be used as a key in unordered containers via + * `boost::hash` or any other `hash_append`-compatible hasher. + * + * @tparam Hasher A `hash_append`-compatible hasher type. + * @param h The hasher to feed bytes into. + * @param pk The key whose bytes are appended. + */ template void hash_append(Hasher& h, PublicKey const& pk) @@ -136,6 +158,13 @@ hash_append(Hasher& h, PublicKey const& pk) h(pk.data(), pk.size()); } +/** Serialization bridge between `STBlob` fields and `PublicKey` values. + * + * This specialization plugs `PublicKey` into XRPL's typed serialization + * framework. It allows `get` and `set` on `STBlob` + * fields in serialized ledger objects and transactions without any + * conversion boilerplate at call sites. + */ template <> struct STExchange { @@ -143,12 +172,14 @@ struct STExchange using value_type = PublicKey; + /** Read a `PublicKey` from an `STBlob` field into `t`. */ static void get(std::optional& t, STBlob const& u) { t.emplace(Slice(u.data(), u.size())); } + /** Write a `PublicKey` into a new `STBlob` for the given field. */ static std::unique_ptr set(SField const& f, PublicKey const& t) { @@ -158,55 +189,86 @@ struct STExchange //------------------------------------------------------------------------------ +/** Encode a public key as a Base58Check string with a token-type prefix. + * + * @param type The `TokenType` prefix to use (e.g. `TokenType::NodePublic` + * for validator keys, `TokenType::AccountPublic` for signing keys). + * @param pk The key to encode. + * @return The Base58Check-encoded string. + */ inline std::string toBase58(TokenType type, PublicKey const& pk) { return encodeBase58Token(type, pk.data(), pk.size()); } +/** Decode a Base58Check-encoded public key. + * + * Validates the token-type prefix and that the decoded bytes represent a + * recognized key format. Safe to call on untrusted input. + * + * @param type The expected `TokenType` prefix. + * @param s The Base58Check-encoded string to decode. + * @return A `PublicKey` on success, or `std::nullopt` if the string is + * malformed, uses the wrong token type, or the decoded bytes are not + * a valid secp256k1 or Ed25519 key. + */ template <> std::optional parseBase58(TokenType type, std::string const& s); -enum class ECDSACanonicality { Canonical, FullyCanonical }; +/** Canonicality level of a DER-encoded secp256k1 ECDSA signature. + * + * For any signed message, both `(R, S)` and `(R, G-S)` are mathematically + * valid ECDSA signatures (where G is the secp256k1 curve order). Accepting + * both enables transaction malleability attacks. XRPL prevents this by + * requiring *fully canonical* signatures — where `S ≤ G-S` — for new + * transactions. + */ +enum class ECDSACanonicality { + /** Both R and S are in `[1, G)` with no redundant zero padding, but + * `S > G/2`. Structurally valid; may be accepted in legacy contexts. */ + Canonical, + /** Both R and S are in `[1, G)` and `S ≤ G-S`, making the signature + * unique and immune to the malleability flip. Required for new XRPL + * transactions. */ + FullyCanonical +}; -/** Determines the canonicality of a signature. - - A canonical signature is in its most reduced form. - For example the R and S components do not contain - additional leading zeroes. However, even in - canonical form, (R,S) and (R,G-S) are both - valid signatures for message M. - - Therefore, to prevent malleability attacks we - define a fully canonical signature as one where: - - R < G - S - - where G is the curve order. - - This routine returns std::nullopt if the format - of the signature is invalid (for example, the - points are encoded incorrectly). - - @return std::nullopt if the signature fails - validity checks. - - @note Only the format of the signature is checked, - no verification cryptography is performed. -*/ +/** Determine the canonicality of a DER-encoded secp256k1 ECDSA signature. + * + * Validates the DER structure (`0x30 0x02 0x02 `), checks + * that R and S are properly encoded integers (no negative encoding, no + * redundant zero padding), and compares them against the secp256k1 curve + * order G. Returns `FullyCanonical` when `S ≤ G-S`, `Canonical` when + * `S > G-S` but the signature is otherwise structurally sound. + * + * @param sig DER-encoded ECDSA signature to examine. + * @return `ECDSACanonicality::FullyCanonical` if `S ≤ G-S`, + * `ECDSACanonicality::Canonical` if `S > G-S` but structurally valid, + * or `std::nullopt` if the encoding is malformed (wrong header bytes, + * invalid integer components, R or S outside the curve order, or + * trailing bytes present). + * @note Only the structure and canonicality of the encoding are checked; + * no cryptographic verification is performed. + */ std::optional ecdsaCanonicality(Slice const& sig); -/** Returns the type of public key. - - @return std::nullopt If the public key does not - represent a known type. -*/ +/** Determine the algorithm encoded in a public key. + * + * Uses the lead byte as a self-describing type tag: `0xED` → Ed25519; + * `0x02`/`0x03` → secp256k1 compressed. Any other lead byte, or a slice + * that is not exactly 33 bytes, is unrecognized. + * + * @return The detected `KeyType`, or `std::nullopt` if the bytes do not + * match a known key format. + */ /** @{ */ [[nodiscard]] std::optional publicKeyType(Slice const& slice); +/** @copydoc publicKeyType(Slice const&) */ [[nodiscard]] inline std::optional publicKeyType(PublicKey const& publicKey) { @@ -214,7 +276,24 @@ publicKeyType(PublicKey const& publicKey) } /** @} */ -/** Verify a secp256k1 signature on the digest of a message. */ +/** Verify a secp256k1 ECDSA signature against a pre-computed digest. + * + * Validates DER structure and canonicality before calling libsecp256k1. + * When `mustBeFullyCanonical` is `false` and the signature is merely + * canonical (S > G/2), the S component is normalized to its low form via + * `secp256k1_ecdsa_signature_normalize` before verification — preserving + * backward compatibility without accepting truly malformed encodings. + * + * @param publicKey A secp256k1 public key. Passing an Ed25519 key calls + * `LogicError` (programming error). + * @param digest The 256-bit digest over which the signature was produced. + * @param sig DER-encoded ECDSA signature. + * @param mustBeFullyCanonical If `true` (default), reject signatures where + * `S > G/2`. If `false`, accept them after S normalization. + * @return `true` if the signature is cryptographically valid for the given + * key and digest; `false` for any structural, canonicality, or + * cryptographic failure. + */ [[nodiscard]] bool verifyDigest( PublicKey const& publicKey, @@ -222,22 +301,65 @@ verifyDigest( Slice const& sig, bool mustBeFullyCanonical = true) noexcept; -/** Verify a signature on a message. - With secp256k1 signatures, the data is first hashed with - SHA512-Half, and the resulting digest is signed. -*/ +/** Verify a signature over a raw message for either supported key type. + * + * Dispatches on the cryptosystem detected from `publicKey`: + * - **secp256k1**: hashes `m` with SHA512-Half (256-bit digest) and + * delegates to `verifyDigest` with `mustBeFullyCanonical = true`. + * - **Ed25519**: checks that the signature scalar S is below the Ed25519 + * subgroup order, then calls the underlying `ed25519_sign_open` library + * after stripping the XRPL-specific `0xED` prefix byte that the library + * does not understand. + * + * @param publicKey The public key to verify against. + * @param m The message that was signed (raw bytes, not pre-hashed). + * @param sig The signature to verify. + * @return `true` if the signature is valid; `false` for any failure + * including unrecognized key type, non-canonical signature, or + * cryptographic mismatch. + */ [[nodiscard]] bool verify(PublicKey const& publicKey, Slice const& m, Slice const& sig) noexcept; -/** Calculate the 160-bit node ID from a node public key. */ +/** Derive the 160-bit node identity from a public key. + * + * Applies RIPEMD-160(SHA-256(pubkey)) to produce the `NodeID` used in the + * peer-to-peer layer for validator routing and consensus tracking. + * + * @param pk The validator's public key (secp256k1 or Ed25519). + * @return The 160-bit `NodeID` identifying the validator on the network. + */ NodeID calcNodeID(PublicKey const&); +/** Derive the 160-bit on-ledger account address from a public key. + * + * Applies RIPEMD-160(SHA-256(pubkey)) — the same algorithm used in + * Bitcoin — to produce the `AccountID` that identifies the account on the + * XRP Ledger. + * + * @param pk The account's public key. + * @return The `AccountID` corresponding to `pk`. + * @note The implementation lives in `AccountID.cpp` rather than + * `PublicKey.cpp` due to header dependency ordering constraints. + */ // VFALCO This belongs in AccountID.h but // is here because of header issues AccountID calcAccountID(PublicKey const& pk); +/** Format a human-readable peer fingerprint for diagnostic logging. + * + * Produces a string of the form + * `"IP Address: [, Public Key: ][, Id: ]"` suitable + * for audit and connection-lifecycle log messages. + * + * @param address The peer's IP endpoint (always included). + * @param publicKey The peer's node public key, encoded as `NodePublic` + * Base58; omitted if not yet known (e.g., before the handshake). + * @param id An optional session identifier string; omitted if absent. + * @return A formatted fingerprint string. + */ inline std::string getFingerprint( beast::IP::Endpoint const& address, @@ -260,7 +382,22 @@ getFingerprint( //------------------------------------------------------------------------------ -namespace json { +/** Deserialize a `PublicKey` from a JSON field value. + * + * Accepts three formats in order: + * 1. Lowercase hex string of the raw 33-byte key. + * 2. `NodePublic` Base58Check encoding (validator keys). + * 3. `AccountPublic` Base58Check encoding (signing keys). + * + * This covers the variety of formats that appear in RPC requests and + * configuration files. + * + * @param v The JSON object to read from. + * @param field The field whose value is decoded. + * @return The decoded `PublicKey`. + * @throws `JsonTypeMismatchError` if the field value does not match any + * recognized format. + */ template <> inline xrpl::PublicKey getOrThrow(json::Value const& v, xrpl::SField const& field) diff --git a/include/xrpl/protocol/Quality.h b/include/xrpl/protocol/Quality.h index 115e4498df..e77335fa0f 100644 --- a/include/xrpl/protocol/Quality.h +++ b/include/xrpl/protocol/Quality.h @@ -1,3 +1,14 @@ +/** @file + * Defines `Quality` and `TAmounts`, the core exchange-rate abstractions + * used by XRPL's on-ledger decentralized exchange (DEX). + * + * `Quality` is the sortable representation of a currency exchange rate. + * The offer-crossing engine — ranking offers, scaling partial fills, and + * composing multi-hop paths — is expressed entirely in terms of these types. + * + * @see QualityFunction.h for the continuous AMM price-function extension. + */ + #pragma once #include @@ -12,35 +23,50 @@ namespace xrpl { -/** Represents a pair of input and output currencies. - - The input currency can be converted to the output - currency by multiplying by the rate, represented by - Quality. - - For offers, "in" is always TakerPays and "out" is - always TakerGets. -*/ +/** A typed pair of input and output amounts representing one side of a trade. + * + * For offers on the DEX, `in` is always `TakerPays` and `out` is always + * `TakerGets`. The template parameters allow instantiation over + * `STAmount`, `IOUAmount`, `XRPAmount`, and `MPTAmount`. + * + * @tparam In Type of the input (paying) amount. + * @tparam Out Type of the output (receiving) amount. + */ template struct TAmounts { TAmounts() = default; + /** Construct a zero-valued pair. */ TAmounts(beast::Zero, beast::Zero) : in(beast::kZERO), out(beast::kZERO) { } + /** Construct from explicit in and out amounts. + * + * @param in The input (TakerPays) amount. + * @param out The output (TakerGets) amount. + */ TAmounts(In in, Out out) : in(std::move(in)), out(std::move(out)) { } - /** Returns `true` if either quantity is not positive. */ + /** Returns `true` if either quantity is not positive. + * + * Used by the offer-crossing engine to skip exhausted or invalid offers + * without further computation. + */ [[nodiscard]] bool empty() const noexcept { return in <= beast::kZERO || out <= beast::kZERO; } + /** Adds `rhs` component-wise to this pair. + * + * @param rhs The amounts to add. + * @return Reference to `*this`. + */ TAmounts& operator+=(TAmounts const& rhs) { @@ -49,6 +75,11 @@ struct TAmounts return *this; } + /** Subtracts `rhs` component-wise from this pair. + * + * @param rhs The amounts to subtract. + * @return Reference to `*this`. + */ TAmounts& operator-=(TAmounts const& rhs) { @@ -57,12 +88,14 @@ struct TAmounts return *this; } - In in{}; - Out out{}; + In in{}; /**< Input (TakerPays) amount. */ + Out out{}; /**< Output (TakerGets) amount. */ }; +/** Canonical `TAmounts` alias used by the `STAmount`-based offer-crossing path. */ using Amounts = TAmounts; +/** Returns `true` when both sides of two `TAmounts` pairs are equal. */ template bool operator==(TAmounts const& lhs, TAmounts const& rhs) noexcept @@ -70,6 +103,7 @@ operator==(TAmounts const& lhs, TAmounts const& rhs) noexcept return lhs.in == rhs.in && lhs.out == rhs.out; } +/** Returns `true` when either side of two `TAmounts` pairs differs. */ template bool operator!=(TAmounts const& lhs, TAmounts const& rhs) noexcept @@ -79,54 +113,107 @@ operator!=(TAmounts const& lhs, TAmounts const& rhs) noexcept //------------------------------------------------------------------------------ -// XRPL specific constant used for parsing qualities and other things +/** Unity exchange rate (1:1), scaled to XRPL's 9-decimal fixed-point precision. + * + * Appears throughout offer parsing and fee calculations wherever a 1:1 + * exchange rate must be expressed as a raw integer. + */ #define QUALITY_ONE 1'000'000'000 -/** Represents the logical ratio of output currency to input currency. - Internally this is stored using a custom floating point representation, - as the inverse of the ratio, so that quality will be descending in - a sequence of actual values that represent qualities. -*/ +/** The exchange rate of an offer, stored as an inverted packed floating-point + * integer so that higher-quality offers sort first under plain integer comparison. + * + * A `Quality` encodes the ratio `out / in` (TakerGets / TakerPays): how much + * output the taker receives per unit of input. Higher quality is better for + * the taker (more output per unit of input). + * + * The internal `uint64_t` uses the same bit layout as `STAmount` IOU encoding: + * the top 8 bits hold a biased exponent (stored value = actual exponent + 100) + * and the lower 56 bits hold an unsigned mantissa. Critically, the integer + * value is **inverted** relative to the economic concept — a *higher* quality + * corresponds to a *lower* `uint64_t` — so that ascending integer order in the + * ledger's offer directories corresponds to descending quality, allowing the + * best offers to be processed first. + * + * @note The increment/decrement operators navigate the discrete floating-point + * grid by modifying `value_` by one ULP. The representation may become + * non-canonical after such operations. + * + * @see composedQuality() for two-hop path composition. + * @see QualityFunction.h for the continuous AMM extension of this type. + */ class Quality { public: - // Type of the internal representation. Higher qualities - // have lower unsigned integer representations. + /** Underlying storage type. Higher qualities have lower integer values. */ using value_type = std::uint64_t; + /** Minimum valid tick size (significant decimal digits) for `round()`. */ static int const kMIN_TICK_SIZE = 3; + + /** Maximum valid tick size (significant decimal digits) for `round()`. */ static int const kMAX_TICK_SIZE = 16; private: - // This has the same representation as STAmount, see the comment on the - // STAmount. However, this class does not always use the canonical - // representation. In particular, the increment and decrement operators may - // cause a non-canonical representation. + // Packed 64-bit encoding: bits [63:56] = biased exponent (actual + 100), + // bits [55:0] = mantissa. Identical to the STAmount IOU wire format. + // May be non-canonical after operator++ / operator--. value_type value_; public: Quality() = default; - /** Create a quality from the integer encoding of an STAmount */ + /** Construct from a raw packed integer in STAmount encoding. + * + * The top 8 bits are the biased exponent (actual exponent + 100) and + * the bottom 56 bits are the mantissa. Higher integers denote lower + * (worse) quality because the internal ordering is inverted. + * + * @param value Packed 64-bit quality value. + */ explicit Quality(std::uint64_t value); - /** Create a quality from the ratio of two amounts. */ + /** Construct from an `STAmount` in/out pair encoding `out / in`. + * + * Calls `getRate(amount.out, amount.in)` to produce the packed value. + * Neither side should be zero. + * + * @param amount Offer amounts: `in` = TakerPays, `out` = TakerGets. + */ explicit Quality(Amounts const& amount); - /** Create a quality from the ratio of two amounts. */ + /** Construct from a typed in/out pair by converting to `STAmount` first. + * + * @tparam In Input amount type (e.g., `XRPAmount`, `IOUAmount`). + * @tparam Out Output amount type. + * @param amount The typed offer amounts. + */ template explicit Quality(TAmounts const& amount) : Quality(Amounts(toSTAmount(amount.in), toSTAmount(amount.out))) { } - /** Create a quality from the ratio of two amounts. */ + /** Construct from explicit out and in amounts by converting to `STAmount`. + * + * @tparam In Input amount type. + * @tparam Out Output amount type. + * @param out The output (TakerGets) amount. + * @param in The input (TakerPays) amount. + */ template Quality(Out const& out, In const& in) : Quality(Amounts(toSTAmount(in), toSTAmount(out))) { } - /** Advances to the next higher quality level. */ + /** Advance to the next higher quality level. + * + * Because the internal encoding is inverted, this decrements the stored + * integer by one ULP. Used during offer-book traversal to step the + * crossing price up by the smallest representable increment. + * + * @pre `value_ > 0`; underflow is asserted. + */ /** @{ */ Quality& operator++(); @@ -135,7 +222,13 @@ public: operator++(int); /** @} */ - /** Advances to the next lower quality level. */ + /** Retreat to the next lower quality level. + * + * Because the internal encoding is inverted, this increments the stored + * integer by one ULP. + * + * @pre `value_ < UINT64_MAX`; overflow is asserted. + */ /** @{ */ Quality& operator--(); @@ -144,65 +237,184 @@ public: operator--(int); /** @} */ - /** Returns the quality as STAmount. */ + /** Decode the packed quality value into an `STAmount` exchange rate. + * + * The returned amount represents the rate `out / in` in the IOU + * floating-point format. Callers use this when passing the quality + * to `mulRound` / `divRound` for proportional scaling. + * + * @return The exchange rate as an `STAmount`. + */ [[nodiscard]] STAmount rate() const { return amountFromQuality(value_); } - /** Returns the quality rounded up to the specified number - of decimal digits. - */ + /** Round the quality's mantissa up to `tickSize` significant decimal digits. + * + * Used for tick-size enforcement: coarsens the price grid so that offers + * differing only in low-order digits are treated as equivalent. Rounding + * is always upward (ceiling), which makes the encoded rate slightly higher + * (worse for the taker) and prevents a rounded quality from being mistakenly + * ranked better than the original. + * + * @param tickSize Number of significant digits to retain. Must be in + * `[kMIN_TICK_SIZE, kMAX_TICK_SIZE]`; enforcement is the caller's + * responsibility. + * @return A new `Quality` with a rounded-up mantissa and unchanged exponent. + */ [[nodiscard]] Quality round(int tickSize) const; - /** Returns the scaled amount with in capped. - Math is avoided if the result is exact. The output is clamped - to prevent money creation. - */ + /** Scale an offer's amounts down so that the input does not exceed `limit`. + * + * If `amount.in > limit`, sets `in = limit` and recomputes `out` + * proportionally via `divRound`. The computed output is clamped to + * `amount.out` if arithmetic would produce a larger value, preventing + * money creation due to rounding. Returns `amount` unchanged when + * `amount.in <= limit`. + * + * @param amount Current offer amounts (`in` = TakerPays, `out` = TakerGets). + * @param limit Maximum allowed input amount. + * @return Scaled amounts satisfying `in <= limit` and `out <= amount.out`. + * @note Uses `divRound` (legacy rounding that ignores low-order bits). + * Use `ceilInStrict` when full-precision rounding is required. + */ [[nodiscard]] Amounts ceilIn(Amounts const& amount, STAmount const& limit) const; + /** Scale a typed offer's amounts down so that the input does not exceed `limit`. + * + * Converts both sides to `STAmount`, delegates to the `STAmount` overload, + * then converts the result back to the typed amounts. + * + * @tparam In Input amount type. + * @tparam Out Output amount type. + * @param amount Current offer amounts. + * @param limit Maximum allowed input amount. + * @return Scaled amounts satisfying `in <= limit` and `out <= amount.out`. + */ template [[nodiscard]] TAmounts ceilIn(TAmounts const& amount, In const& limit) const; - // Some of the underlying rounding functions called by ceil_in() ignored - // low order bits that could influence rounding decisions. This "strict" - // method uses underlying functions that pay attention to all the bits. + /** Scale an offer's amounts down so that the input does not exceed `limit`, + * using full-precision rounding. + * + * Identical to `ceilIn` except it delegates to `divRoundStrict`, which + * considers all low-order bits that `divRound` ignores. Introduced to + * fix subtle rounding bugs where a borderline result could influence + * whether an offer crosses. + * + * @param amount Current offer amounts. + * @param limit Maximum allowed input amount. + * @param roundUp Whether to round the recomputed output up (`true`) or + * down (`false`). + * @return Scaled amounts satisfying `in <= limit` and `out <= amount.out`. + */ [[nodiscard]] Amounts ceilInStrict(Amounts const& amount, STAmount const& limit, bool roundUp) const; + /** Scale a typed offer's amounts down so that the input does not exceed `limit`, + * using full-precision rounding. + * + * @tparam In Input amount type. + * @tparam Out Output amount type. + * @param amount Current offer amounts. + * @param limit Maximum allowed input amount. + * @param roundUp Whether to round the recomputed output up or down. + * @return Scaled amounts satisfying `in <= limit` and `out <= amount.out`. + */ template [[nodiscard]] TAmounts ceilInStrict(TAmounts const& amount, In const& limit, bool roundUp) const; - /** Returns the scaled amount with out capped. - Math is avoided if the result is exact. The input is clamped - to prevent money creation. - */ + /** Scale an offer's amounts down so that the output does not exceed `limit`. + * + * If `amount.out > limit`, sets `out = limit` and recomputes `in` + * proportionally via `mulRound`. The computed input is clamped to + * `amount.in` if arithmetic would produce a larger value, preventing + * money creation due to rounding. Returns `amount` unchanged when + * `amount.out <= limit`. + * + * @param amount Current offer amounts. + * @param limit Maximum allowed output amount. + * @return Scaled amounts satisfying `out <= limit` and `in <= amount.in`. + * @note Uses `mulRound` (legacy rounding that ignores low-order bits). + * Use `ceilOutStrict` when full-precision rounding is required. + */ [[nodiscard]] Amounts ceilOut(Amounts const& amount, STAmount const& limit) const; + /** Scale a typed offer's amounts down so that the output does not exceed `limit`. + * + * Converts both sides to `STAmount`, delegates to the `STAmount` overload, + * then converts the result back to the typed amounts. + * + * @tparam In Input amount type. + * @tparam Out Output amount type. + * @param amount Current offer amounts. + * @param limit Maximum allowed output amount. + * @return Scaled amounts satisfying `out <= limit` and `in <= amount.in`. + */ template [[nodiscard]] TAmounts ceilOut(TAmounts const& amount, Out const& limit) const; - // Some of the underlying rounding functions called by ceil_out() ignored - // low order bits that could influence rounding decisions. This "strict" - // method uses underlying functions that pay attention to all the bits. + /** Scale an offer's amounts down so that the output does not exceed `limit`, + * using full-precision rounding. + * + * Identical to `ceilOut` except it delegates to `mulRoundStrict`, which + * considers all low-order bits that `mulRound` ignores. + * + * @param amount Current offer amounts. + * @param limit Maximum allowed output amount. + * @param roundUp Whether to round the recomputed input up (`true`) or + * down (`false`). + * @return Scaled amounts satisfying `out <= limit` and `in <= amount.in`. + */ [[nodiscard]] Amounts ceilOutStrict(Amounts const& amount, STAmount const& limit, bool roundUp) const; + /** Scale a typed offer's amounts down so that the output does not exceed `limit`, + * using full-precision rounding. + * + * @tparam In Input amount type. + * @tparam Out Output amount type. + * @param amount Current offer amounts. + * @param limit Maximum allowed output amount. + * @param roundUp Whether to round the recomputed input up or down. + * @return Scaled amounts satisfying `out <= limit` and `in <= amount.in`. + */ template [[nodiscard]] TAmounts ceilOutStrict(TAmounts const& amount, Out const& limit, bool roundUp) const; private: - // The ceil_in and ceil_out methods that deal in TAmount all convert - // their arguments to STAmount and convert the result back to TAmount. - // This helper function takes care of all the conversion operations. + /** Shared implementation for all typed `ceilIn`/`ceilOut` overloads. + * + * Converts `amount` and `limit` to `STAmount`, calls `ceilFunction` (a + * member-function pointer to one of the `STAmount`-based overloads), and + * converts the result back to `TAmounts`. Returns `amount` + * unchanged when `limitCmp <= limit` (i.e., the limit is not binding). + * + * The variadic `Round...` pack forwards an optional `bool roundUp` argument + * to strict variants without requiring separate instantiations. + * + * @tparam In Input amount type. + * @tparam Out Output amount type. + * @tparam Lim Limit amount type (same as `In` or `Out`). + * @tparam FnPtr Pointer to the `STAmount`-based ceil member function. + * @tparam Round Empty or `{bool}` — forwarded as `roundUp`. + * @param amount Current typed offer amounts. + * @param limit The cap to apply. + * @param limitCmp The side of `amount` to compare against `limit` + * (either `amount.in` or `amount.out`). + * @param ceilFunction Member-function pointer to dispatch to. + * @param round Optional rounding direction (strict variants only). + * @return Scaled `TAmounts`. + */ template ... Round> [[nodiscard]] TAmounts ceilTAmountsHelper( @@ -213,46 +425,53 @@ private: Round... round) const; public: - /** Returns `true` if lhs is lower quality than `rhs`. - Lower quality means the taker receives a worse deal. - Higher quality is better for the taker. - */ + /** Returns `true` if `lhs` is lower quality (worse for the taker) than `rhs`. + * + * Because the internal encoding is inverted, a lower quality corresponds + * to a *higher* stored integer, so this compares `lhs.value_ > rhs.value_`. + */ friend bool operator<(Quality const& lhs, Quality const& rhs) noexcept { return lhs.value_ > rhs.value_; } + /** Returns `true` if `lhs` is higher quality (better for the taker) than `rhs`. */ friend bool operator>(Quality const& lhs, Quality const& rhs) noexcept { return lhs.value_ < rhs.value_; } + /** Returns `true` if `lhs` is lower or equal quality to `rhs`. */ friend bool operator<=(Quality const& lhs, Quality const& rhs) noexcept { return !(lhs > rhs); } + /** Returns `true` if `lhs` is higher or equal quality to `rhs`. */ friend bool operator>=(Quality const& lhs, Quality const& rhs) noexcept { return !(lhs < rhs); } + /** Returns `true` if both qualities encode the same exchange rate. */ friend bool operator==(Quality const& lhs, Quality const& rhs) noexcept { return lhs.value_ == rhs.value_; } + /** Returns `true` if the two qualities encode different exchange rates. */ friend bool operator!=(Quality const& lhs, Quality const& rhs) noexcept { return !(lhs == rhs); } + /** Write the raw packed integer value of the quality to an output stream. */ friend std::ostream& operator<<(std::ostream& os, Quality const& quality) { @@ -260,8 +479,18 @@ public: return os; } - // return the relative distance (relative error) between two qualities. This - // is used for testing only. relative distance is abs(a-b)/min(a,b) + /** Return the relative error between two quality values: `|a - b| / min(a, b)`. + * + * Extracts the exponent and mantissa from each packed value, scales them + * to a common exponent, and returns the normalized distance. Used only + * in unit tests to verify that two qualities are sufficiently close. + * + * @param q1 First quality; must be non-zero (asserted). + * @param q2 Second quality; must be non-zero (asserted). + * @return `|q1 - q2| / min(q1, q2)` as a `double`. + * @note For testing only. Production code should compare with the + * relational operators. + */ friend double relativeDistance(Quality const& q1, Quality const& q2) { @@ -284,9 +513,8 @@ public: double const maxVD = (expDiff != 0) ? maxVMantissa * pow(10, expDiff) : static_cast(maxVMantissa); - // maxVD and minVD are scaled so they have the same exponents. Dividing - // cancels out the exponents, so we only need to deal with the (scaled) - // mantissas + // maxVD and minVD are scaled so they have the same exponent; dividing + // cancels out the exponents, leaving only the normalized mantissa difference. return (maxVD - minVD) / minVD; } }; @@ -315,7 +543,6 @@ template TAmounts Quality::ceilIn(TAmounts const& amount, In const& limit) const { - // Construct a function pointer to the function we want to call. static constexpr Amounts (Quality::*kCEIL_IN_FN_PTR)(Amounts const&, STAmount const&) const = &Quality::ceilIn; @@ -326,7 +553,6 @@ template TAmounts Quality::ceilInStrict(TAmounts const& amount, In const& limit, bool roundUp) const { - // Construct a function pointer to the function we want to call. static constexpr Amounts (Quality::*kCEIL_IN_FN_PTR)(Amounts const&, STAmount const&, bool) const = &Quality::ceilInStrict; @@ -337,7 +563,6 @@ template TAmounts Quality::ceilOut(TAmounts const& amount, Out const& limit) const { - // Construct a function pointer to the function we want to call. static constexpr Amounts (Quality::*kCEIL_OUT_FN_PTR)(Amounts const&, STAmount const&) const = &Quality::ceilOut; @@ -348,17 +573,26 @@ template TAmounts Quality::ceilOutStrict(TAmounts const& amount, Out const& limit, bool roundUp) const { - // Construct a function pointer to the function we want to call. static constexpr Amounts (Quality::*kCEIL_OUT_FN_PTR)(Amounts const&, STAmount const&, bool) const = &Quality::ceilOutStrict; return ceilTAmountsHelper(amount, limit, amount.out, kCEIL_OUT_FN_PTR, roundUp); } -/** Calculate the quality of a two-hop path given the two hops. - @param lhs The first leg of the path: input to intermediate. - @param rhs The second leg of the path: intermediate to output. -*/ +/** Compute the effective end-to-end exchange rate for a two-hop path. + * + * If the first hop converts A→B at rate `lhs` and the second converts B→C + * at rate `rhs`, the composed quality is their product, re-encoded into the + * packed 64-bit format. Used by the pathfinding engine to rank multi-hop + * routes against single-hop offers on a common scale. + * + * @param lhs Quality of the first leg (input → intermediate currency). + * @param rhs Quality of the second leg (intermediate → output currency). + * @return Composed quality representing the overall A→C exchange rate. + * @note Both input rates must be non-zero (asserted at runtime). The + * composed exponent must fit in 8 bits (i.e., actual exponent in + * [-99, 155]); astronomically large or small paths will assert. + */ Quality composedQuality(Quality const& lhs, Quality const& rhs); diff --git a/include/xrpl/protocol/QualityFunction.h b/include/xrpl/protocol/QualityFunction.h index f7f92e50da..6549eb6038 100644 --- a/include/xrpl/protocol/QualityFunction.h +++ b/include/xrpl/protocol/QualityFunction.h @@ -6,52 +6,152 @@ namespace xrpl { -/** Average quality of a path as a function of `out`: q(out) = m * out + b, - * where m = -1 / poolGets, b = poolPays / poolGets. If CLOB offer then - * `m` is equal to 0 `b` is equal to the offer's quality. The function - * is derived by substituting `in` in q = out / in with the swap out formula - * for `in`: - * in = [(poolGets * poolPays) / (poolGets - out) - poolPays] / (1 - tfee) - * and combining the function for multiple steps. The function is used - * to limit required output amount when quality limit is provided in one - * path optimization. +/** Average quality of a payment strand expressed as a linear function of output. + * + * Models the relationship `q(out) = m_ * out + b_`, where `q` is the average + * exchange rate (quality) that a strand delivers when it produces `out` units. + * This analytical model lets `StrandFlow::limitOut()` compute — without + * simulation — the maximum output the strand may produce before AMM price + * impact degrades the average quality below a caller-supplied limit. + * + * **Derivation.** For an AMM step with pool balances `poolGets` (input side) + * and `poolPays` (output side) and fee multiplier `cfee = 1 - tfee`, the + * constant-product swap formula gives: + * @code + * in = [(poolGets * poolPays) / (poolGets - out) - poolPays] / cfee + * @endcode + * Substituting into `q = out / in` and linearising yields: + * @code + * m = -cfee / poolGets (always negative for a valid AMM step) + * b = poolPays * cfee / poolGets + * @endcode + * + * **Multi-hop composition.** For strands with sequential steps (e.g. a + * transfer-fee hop preceding an AMM hop), `combine()` chains two quality + * functions analytically. `StrandFlow::limitOut()` calls `combine()` in a + * loop over all steps to accumulate a single QF representing the whole strand. + * + * **Two construction modes** are selected via tag dispatch: + * - `AMMTag` — variable quality; slope and intercept derived from pool balances. + * - `CLOBLikeTag` — constant quality (`m_ = 0`); used for plain CLOB orders and + * for AMM offers in multi-path mode, where each path's AMM allocation is fixed. + * + * @note The linear approximation is exact for *average* quality but not for + * instantaneous (marginal) quality, which is quadratic. Using averages + * keeps composition algebraically simple while still providing a + * conservative, analytically tractable bound. + * + * @see StrandFlow.h `limitOut()` — primary consumer of this class. */ class QualityFunction { private: - // slope + /** Slope of the quality–output line (`-cfee / poolGets` for AMM; 0 for CLOB). */ Number m_; - // intercept + /** Intercept of the quality–output line (`poolPays * cfee / poolGets` for AMM; + * `1 / quality.rate()` for CLOB). */ Number b_; - // seated if QF is for CLOB offer. + /** Cached constant quality; seated only when `m_ == 0` (CLOB-like function). */ std::optional quality_; public: + /** Tag type that selects the AMM constructor (variable-quality path step). */ struct AMMTag { }; - // AMMOffer for multi-path is like CLOB, i.e. the offer size - // changes proportionally to its quality. + /** Tag type that selects the CLOB-like constructor (constant-quality path step). + * + * Used for both plain CLOB orders and AMM offers operating in multi-path + * mode, where the AMM offer size scales proportionally with quality just + * like a CLOB, making the effective quality constant from this sub-path's + * perspective. + */ struct CLOBLikeTag { }; + + /** Construct a constant-quality (CLOB-like) quality function. + * + * Sets `m_ = 0` and `b_ = 1 / quality.rate()`. `quality_` is seated so + * that `isConst()` returns `true` and `StrandFlow::limitOut()` skips the + * output cap. + * + * @param quality The fixed exchange rate of this path step. + * @throws std::runtime_error if `quality.rate()` is zero, which would + * make the intercept infinite. + */ QualityFunction(Quality const& quality, CLOBLikeTag); + + /** Construct a variable-quality (AMM) quality function from pool balances. + * + * Derives the slope and intercept from the constant-product swap formula: + * @code + * m_ = -cfee / amounts.in + * b_ = amounts.out * cfee / amounts.in + * @endcode + * where `cfee = feeMult(tfee)`. `quality_` is left empty; `isConst()` + * returns `false`. + * + * @tparam TIn Input amount type (e.g. `XRPAmount`, `IOUAmount`). + * @tparam TOut Output amount type. + * @param amounts Current AMM pool balances: `amounts.in` is the input-side + * pool depth, `amounts.out` is the output-side pool depth. + * @param tfee AMM trading fee in the same units as `feeMult()` expects. + * @throws std::runtime_error if either pool balance is zero or negative, + * which would cause division-by-zero in downstream arithmetic. + */ template QualityFunction(TAmounts const& amounts, std::uint32_t tfee, AMMTag); - /** Combines QF with the next step QF + /** Chain this quality function with the next path step's quality function. + * + * Applies linear function composition in reciprocal-rate space: + * @code + * m_ += b_ * qf.m_; + * b_ *= qf.b_; + * @endcode + * If the combined slope becomes nonzero, `quality_` is cleared to reflect + * that the resulting function is no longer constant and `outFromAvgQ()` + * must be used rather than a simple pass/fail quality check. + * + * @param qf Quality function for the next step to compose in. */ void combine(QualityFunction const& qf); - /** Find output to produce the requested - * average quality. - * @param quality requested average quality (quality limit) + /** Solve for the maximum output at which average quality meets the given limit. + * + * Inverts `q(out) = m_ * out + b_` by substituting `q = 1 / quality.rate()`: + * @code + * out = (1 / quality.rate() - b_) / m_ + * @endcode + * The rounding mode is set to `Upward` during the calculation so the + * returned bound is conservative: because `m_` is negative, dividing an + * upward-rounded numerator by a negative slope yields a result that rounds + * down, ensuring the engine never requests marginally more output than the + * quality constraint allows. + * + * Returns `std::nullopt` in three cases: + * - `m_ == 0`: the function is constant (CLOB-like); quality either passes + * or fails uniformly, so no output cap is meaningful. + * - `quality.rate() == 0`: guards against division-by-zero when forming + * `1 / rate`. + * - `out <= 0`: the quality limit cannot be achieved at any positive output; + * the strand is effectively dead for this constraint. + * + * @param quality The minimum acceptable average exchange rate (quality limit). + * @return The output amount at which the strand's average quality equals + * `quality`, or `std::nullopt` if the cap is inapplicable or infeasible. */ std::optional outFromAvgQ(Quality const& quality); - /** Return true if the quality function is constant + /** Return `true` if this quality function is constant (CLOB-like). + * + * A constant function has `m_ == 0`: the average quality is the same + * regardless of output size. `StrandFlow::limitOut()` treats a constant + * function as a signal to skip the output cap and return `remainingOut` + * unchanged. */ [[nodiscard]] bool isConst() const @@ -59,6 +159,13 @@ public: return quality_.has_value(); } + /** Return the cached constant quality, if any. + * + * Seated only when `isConst() == true` (i.e., this is a CLOB-like + * function constructed via `CLOBLikeTag`). Returns `std::nullopt` for + * variable-quality (AMM) functions and for any combined function whose + * slope became nonzero after `combine()`. + */ [[nodiscard]] std::optional const& quality() const { diff --git a/include/xrpl/protocol/RPCErr.h b/include/xrpl/protocol/RPCErr.h index 1439146e0e..4e34b29006 100644 --- a/include/xrpl/protocol/RPCErr.h +++ b/include/xrpl/protocol/RPCErr.h @@ -1,13 +1,49 @@ #pragma once +/** @file + * Deprecated compatibility shim for the XRPL RPC error API. + * + * Declares `rpcError()` and `isRpcError()` — legacy entry points in the + * `xrpl` namespace that predate the richer `RPC`-namespaced error + * infrastructure in `ErrorCodes.h`. New code should use `RPC::makeError()` + * and `RPC::containsError()` from `ErrorCodes.h` directly. + */ + #include #include namespace xrpl { -// VFALCO NOTE these are deprecated +/** Return `true` if @p jvResult represents an RPC error response. + * + * Duck-types the JSON value by checking for the presence of the `"error"` + * key — the same structural sentinel that `RPC::containsError()` tests, + * making it the direct modern equivalent. + * + * @param jvResult The JSON value to inspect (taken by value, not + * `const` reference — an inefficiency inherited from the original + * implementation that was never corrected given this function's + * deprecated status). + * @return `true` if @p jvResult is a JSON object containing an `"error"` + * member. + * @deprecated Use `RPC::containsError(json)` from `ErrorCodes.h` instead. + */ bool isRpcError(json::Value jvResult); + +/** Construct a fresh JSON error object for the given error code. + * + * Delegates to `RPC::injectError()`, which populates a new `Json::Value` + * object with the canonical `error` token, `error_code`, and + * `error_message` fields drawn from the static `ErrorInfo` registry. + * The return-by-value produces a self-contained error object ready for + * direct return from an RPC handler. + * + * @param iError The RPC error code to encode. + * @return A new `Json::Value` object with `error`, `error_code`, and + * `error_message` populated. + * @deprecated Use `RPC::makeError(code)` from `ErrorCodes.h` instead. + */ json::Value rpcError(ErrorCodeI iError); diff --git a/include/xrpl/protocol/Rate.h b/include/xrpl/protocol/Rate.h index 5dcd62a295..9f7fad56a6 100644 --- a/include/xrpl/protocol/Rate.h +++ b/include/xrpl/protocol/Rate.h @@ -1,3 +1,13 @@ +/** @file + * Defines the `Rate` struct and its arithmetic free functions for applying + * XRPL transfer fees to `STAmount` values. + * + * Transfer rates are billion-scale fractions: `1,000,000,000` is parity + * (no fee). `kPARITY_RATE` is the sentinel for the fee-free common case; + * all six arithmetic functions short-circuit on it without entering the + * `STAmount` multiply/divide path. + */ + #pragma once #include @@ -10,14 +20,27 @@ namespace xrpl { -/** Represents a transfer rate - - Transfer rates are specified as fractions of 1 billion. - For example, a transfer rate of 1% is represented as - 1,010,000,000. -*/ +/** Protocol-level transfer rate, expressed as a fraction of one billion. + * + * A value of `1,000,000,000` means 1:1 — no fee. A value of + * `1,010,000,000` means the sender must deliver 1.01 units for every 1 unit + * the recipient receives (a 1% fee). This scale matches `QUALITY_ONE` in + * `Quality.h`, tying transfer fees directly to the ledger's price + * representation. + * + * `boost::totally_ordered` generates `!=`, `>`, `<=`, and `>=` from + * the manually provided `==` and `<`, keeping the struct concise while + * remaining fully ordered. + * + * @note The default constructor is deleted: a `Rate` with an unspecified + * value is meaningless, and zero would violate the nonzero precondition + * asserted by every arithmetic function in this header. The constructor + * is `explicit` to prevent accidental implicit conversion from the + * large integers that rate values resemble. + */ struct Rate : private boost::totally_ordered { + /** The raw billion-scale rate value as stored in `sfTransferRate`. */ std::uint32_t value; Rate() = delete; @@ -27,18 +50,21 @@ struct Rate : private boost::totally_ordered } }; +/** Returns `true` if both rates have the same billion-scale value. */ inline bool operator==(Rate const& lhs, Rate const& rhs) noexcept { return lhs.value == rhs.value; } +/** Returns `true` if `lhs` is a strictly smaller rate than `rhs`. */ inline bool operator<(Rate const& lhs, Rate const& rhs) noexcept { return lhs.value < rhs.value; } +/** Writes the raw billion-scale rate value to `os`. */ inline std::ostream& operator<<(std::ostream& os, Rate const& rate) { @@ -46,32 +72,126 @@ operator<<(std::ostream& os, Rate const& rate) return os; } +/** Scale an amount by a transfer rate, preserving its asset. + * + * Computes `amount × (rate / 10^9)`. Returns `amount` unchanged when + * `rate == kPARITY_RATE`, avoiding the `STAmount` arithmetic path for the + * common fee-free case. + * + * @param amount The value to scale. + * @param rate The transfer rate to apply; must be nonzero. + * @return The scaled `STAmount` denominated in the same asset as `amount`. + * @pre `rate.value != 0`; asserted in debug builds. + */ STAmount multiply(STAmount const& amount, Rate const& rate); +/** Scale an amount by a transfer rate with controlled rounding, preserving its asset. + * + * Like `multiply()`, but the caller controls rounding direction. Used in + * IOU payment routing where fee calculations stay in a single currency. + * + * @param amount The value to scale. + * @param rate The transfer rate to apply; must be nonzero. + * @param roundUp If `true`, round fractional results toward positive + * infinity; otherwise round toward zero. + * @return The scaled `STAmount` denominated in the same asset as `amount`. + * @pre `rate.value != 0`; asserted in debug builds. + */ STAmount multiplyRound(STAmount const& amount, Rate const& rate, bool roundUp); +/** Scale an amount by a transfer rate with controlled rounding, emitting a specified asset. + * + * Overload for offer-crossing and cross-currency paths where the output + * must be denominated in a different asset than the input. + * + * @param amount The value to scale. + * @param rate The transfer rate to apply; must be nonzero. + * @param asset The asset type of the returned `STAmount`. + * @param roundUp If `true`, round fractional results toward positive + * infinity; otherwise round toward zero. + * @return The scaled `STAmount` denominated in `asset`. + * @pre `rate.value != 0`; asserted in debug builds. + */ STAmount multiplyRound(STAmount const& amount, Rate const& rate, Asset const& asset, bool roundUp); +/** Scale an amount by the inverse of a transfer rate, preserving its asset. + * + * Computes `amount / (rate / 10^9)` — the inverse of `multiply()`. Used + * when back-calculating the gross send amount needed to deliver a given net + * amount after fees. Returns `amount` unchanged for `kPARITY_RATE`. + * + * @param amount The value to scale. + * @param rate The transfer rate to invert; must be nonzero. + * @return The scaled `STAmount` denominated in the same asset as `amount`. + * @pre `rate.value != 0`; asserted in debug builds. + */ STAmount divide(STAmount const& amount, Rate const& rate); +/** Scale an amount by the inverse of a transfer rate with controlled rounding, preserving its asset. + * + * Like `divide()`, but the caller controls rounding direction. Used in + * IOU payment routing for single-currency gross-amount back-calculation. + * + * @param amount The value to scale. + * @param rate The transfer rate to invert; must be nonzero. + * @param roundUp If `true`, round fractional results toward positive + * infinity; otherwise round toward zero. + * @return The scaled `STAmount` denominated in the same asset as `amount`. + * @pre `rate.value != 0`; asserted in debug builds. + */ STAmount divideRound(STAmount const& amount, Rate const& rate, bool roundUp); +/** Scale an amount by the inverse of a transfer rate with controlled rounding, emitting a specified asset. + * + * Overload for offer-crossing and cross-currency paths where the output + * must be denominated in a different asset than the input. + * + * @param amount The value to scale. + * @param rate The transfer rate to invert; must be nonzero. + * @param asset The asset type of the returned `STAmount`. + * @param roundUp If `true`, round fractional results toward positive + * infinity; otherwise round toward zero. + * @return The scaled `STAmount` denominated in `asset`. + * @pre `rate.value != 0`; asserted in debug builds. + */ STAmount divideRound(STAmount const& amount, Rate const& rate, Asset const& asset, bool roundUp); namespace nft { -/** Given a transfer fee (in basis points) convert it to a transfer rate. */ + +/** Convert an NFT transfer fee in basis points to a billion-scale `Rate`. + * + * NFT royalties are stored as a `uint16_t` in basis points (0–50,000 + * representing 0%–50%). Because `Rate` uses `10^9` as its unit, the + * conversion multiplies by `10,000`: a maximum fee of `50,000 bp` becomes + * `500,000,000`, safely below `QUALITY_ONE` and within `uint32_t` range. + * + * @param fee NFT transfer fee in basis points (0–50,000); validated by + * transaction processing before reaching this function. + * @return A `Rate` suitable for passing to `multiply()` or `multiplyRound()`. + * @note Do not call this for ordinary IOU transfer rates — those are already + * billion-scale and should be wrapped in `Rate` directly. + */ Rate transferFeeAsRate(std::uint16_t fee); } // namespace nft -/** A transfer rate signifying a 1:1 exchange */ +/** The 1:1 transfer rate — sender pays exactly what the recipient receives. + * + * Equal to `QUALITY_ONE` (`1,000,000,000`). Every arithmetic function in + * this header returns `amount` unchanged when it detects this value, so + * payment paths through accounts with no transfer fee never enter the + * `STAmount` multiply/divide path. `transferRate()` returns this sentinel + * when an account's `sfTransferRate` field is absent. + * + * @see transferRate() in AccountRootHelpers.h + */ extern Rate const kPARITY_RATE; } // namespace xrpl diff --git a/include/xrpl/protocol/RippleLedgerHash.h b/include/xrpl/protocol/RippleLedgerHash.h index 9dab644663..a1a4ae1759 100644 --- a/include/xrpl/protocol/RippleLedgerHash.h +++ b/include/xrpl/protocol/RippleLedgerHash.h @@ -1,9 +1,31 @@ +/** @file + * Defines the `LedgerHash` type alias used throughout the ledger stack to + * identify closed ledgers by their cryptographic digest. + */ + #pragma once #include namespace xrpl { +/** The SHA-512/256 digest that uniquely identifies a closed XRP Ledger. + * + * A ledger hash is computed over the serialized `LedgerHeader` — covering the + * account-state hash, transaction-set hash, sequence number, close time, drop + * totals, and parent ledger hash — and is 32 bytes wide. + * + * The alias over bare `uint256` serves two purposes: it makes interfaces + * self-documenting at call sites (e.g., `CanonicalTXSet(LedgerHash const&)`), + * and it isolates all ledger-hash usage behind a single name so that a + * tagged variant (`base_uint<256, struct LedgerHashTag>`) can be introduced + * later to prevent cross-domain substitution with transaction hashes or + * account IDs without touching every call site. + * + * @note `LedgerHeader` stores its hash fields as bare `uint256` for historical + * reasons; higher-level APIs (`CanonicalTXSet`, `LedgerHistory`, + * `InboundLedgers`, `RCLValidations`) consistently use this alias. + */ using LedgerHash = uint256; } // namespace xrpl diff --git a/include/xrpl/protocol/Rules.h b/include/xrpl/protocol/Rules.h index fbbd3d8805..609d6b9e3e 100644 --- a/include/xrpl/protocol/Rules.h +++ b/include/xrpl/protocol/Rules.h @@ -8,27 +8,55 @@ namespace xrpl { -/** Check whether a feature is enabled in the current ledger rules +/** Query whether a feature is enabled, with an explicit fallback value. * - * @param feature The feature to be tested. - * @param resultIfNoRules What to return if called from outside a Transactor context. + * Delegates to the thread-local current `Rules` installed by + * `CurrentTransactionRulesGuard`. Use this overload when the desired + * behavior outside a transaction context differs from `false`. + * + * @param feature The amendment ID to test. + * @param resultIfNoRules Value returned when called outside any transaction + * context (i.e. no `CurrentTransactionRulesGuard` is on the call stack). + * @return `true` if the feature is enabled in the current rules; + * `resultIfNoRules` if no rules are installed. */ bool isFeatureEnabled(uint256 const& feature, bool resultIfNoRules); -/** Check whether a feature is enabled in the current ledger rules +/** Query whether a feature is enabled in the current transaction context. * - * @param feature The feature to be tested. + * Delegates to the thread-local current `Rules` installed by + * `CurrentTransactionRulesGuard`. Lower-level protocol code that cannot + * accept a `Rules` parameter (e.g. `STAmount`, `AMMHelpers`) uses this + * function instead. The implicit reliance on thread-local state means + * callers must ensure a `CurrentTransactionRulesGuard` is active on the + * call stack; calling it outside a transaction context silently returns + * `false`. * - * Returns false if no global Rules object is available. i.e. Outside of - * a Transactor context + * @param feature The amendment ID to test. + * @return `true` if the feature is enabled; `false` if no rules are + * installed or the feature is absent from the current ledger's + * amendment set. */ bool isFeatureEnabled(uint256 const& feature); class DigestAwareReadView; -/** Rules controlling protocol behavior. */ +/** Authoritative snapshot of which protocol amendments are active for a ledger. + * + * Every behavioral branch in the transaction engine that depends on a + * conditionally-enabled feature gates through `Rules::enabled()`. `Rules` is + * a value type backed by a `shared_ptr` (pimpl), so copying is + * cheap — only an atomic refcount bump — regardless of how many amendments are + * active. Instances are typically constructed via `makeRulesGivenLedger` and + * installed on the call stack by `CurrentTransactionRulesGuard`. + * + * @note The default constructor is deleted. Every `Rules` instance must carry + * an explicit preset set to prevent accidentally propagating a zero-feature + * state into transaction processing. + * @see makeRulesGivenLedger, CurrentTransactionRulesGuard + */ class Rules { private: @@ -51,11 +79,15 @@ public: Rules() = delete; - /** Construct an empty rule set. - - These are the rules reflected by - the genesis ledger. - */ + /** Construct an empty rule set from a preset collection. + * + * Intended for the genesis ledger, which has no on-ledger amendments yet. + * The preset features are treated as unconditionally enabled and checked + * first by `enabled()`. + * + * @param presets Features that are always enabled regardless of ledger + * state (e.g. features forced on in test or devnet configurations). + */ explicit Rules(std::unordered_set> const& presets); private: @@ -77,28 +109,83 @@ private: presets() const; public: - /** Returns `true` if a feature is enabled. */ + /** Returns `true` if a feature is enabled in this rule set. + * + * Checks the preset collection first (always-on features), then the + * on-ledger amendment set populated from `sfAmendments`. + * + * @param feature The amendment ID to test. + * @return `true` if the feature is in the preset collection or was + * active in the ledger's amendment set at the time this `Rules` + * was constructed. + */ [[nodiscard]] bool enabled(uint256 const& feature) const; /** Returns `true` if two rule sets are identical. - - @note This is for diagnostics. - */ + * + * Comparison is O(1) when both instances carry a digest (the common case + * for ledgers with an amendments SLE): differing digests are immediately + * unequal. Two instances without a digest (genesis state) are considered + * equal. An assertion guards against comparing instances with identical + * digests but differing presets. + * + * @note Intended for diagnostics only, not for load-bearing equality + * decisions in transaction processing. + */ bool operator==(Rules const&) const; + /** Returns `true` if two rule sets differ. + * + * Derived from `operator==`; see its documentation for comparison + * semantics. + */ bool operator!=(Rules const& other) const; }; +/** Returns the active `Rules` for the current thread's transaction context. + * + * The returned reference is valid until the next call to + * `setCurrentTransactionRules` on this thread. Prefer using + * `CurrentTransactionRulesGuard` over calling these functions directly. + * + * @return The currently installed rules, or an empty `optional` if no + * `CurrentTransactionRulesGuard` is on the call stack. + * @see CurrentTransactionRulesGuard + */ std::optional const& getCurrentTransactionRules(); +/** Install `r` as the active rules for the current thread's transaction context. + * + * Beyond storing `r` in the thread-local slot, this function also calls + * `Number::setMantissaScale()` to push the appropriate numeric precision mode: + * `MantissaRange::large` when `featureSingleAssetVault` or + * `featureLendingProtocol` is enabled, `small` otherwise. This push strategy + * avoids per-operation rule lookups inside hot arithmetic paths. + * + * Prefer `CurrentTransactionRulesGuard` over calling this directly, as it + * ensures the previous rules are always restored on scope exit. + * + * @param r The rules to install, or `std::nullopt` to clear the slot. + * @see CurrentTransactionRulesGuard + */ void setCurrentTransactionRules(std::optional r); -/** RAII class to set and restore the current transaction rules +/** RAII guard that installs a `Rules` into the thread-local transaction context. + * + * The constructor calls `setCurrentTransactionRules` with the supplied rules, + * saving the previously active value. The destructor restores that saved value, + * ensuring the thread-local state is always reset even on exception paths. + * Non-copyable to prevent accidental aliasing of the saved state. + * + * Production callers are `Transactor::operator()` and `applySteps.cpp`; test + * code uses this guard to bracket individual feature checks. + * + * @see setCurrentTransactionRules, getCurrentTransactionRules */ class CurrentTransactionRulesGuard { diff --git a/include/xrpl/protocol/SField.h b/include/xrpl/protocol/SField.h index 26f52cd6a9..b533ee832e 100644 --- a/include/xrpl/protocol/SField.h +++ b/include/xrpl/protocol/SField.h @@ -1,3 +1,20 @@ +/** @file + * Compile-time field identification and wire-type catalog for XRPL serialized + * objects. + * + * Every data field that can appear in an XRPL transaction, ledger entry, + * validation, or transaction metadata is identified by a singleton `SField` + * instance declared in this header. The `SerializedTypeID` enum defines the + * recognized wire types. `TypedField` adds a compile-time C++ type so + * that callers can interact with fields in a type-safe way. + * + * @note Some fields distinguish between the default value and the absent + * state. For example, `sfQualityIn` on a trust line with value 0 + * means "no quality set" (absent) versus 1,000,000,000 (parity rate + * when explicitly set). Keep this in mind when testing presence. + * + * @see SField, TypedField, SerializedTypeID + */ #pragma once #include @@ -9,15 +26,6 @@ namespace xrpl { -/* - -Some fields have a different meaning for their - default value versus not present. - Example: - QualityIn on a TrustLine - -*/ - //------------------------------------------------------------------------------ // Forwards @@ -35,6 +43,22 @@ class STVector256; class STCurrency; // NOLINTBEGIN(readability-identifier-naming) +/** Wire-type codes for XRPL binary serialization. + * + * Each value identifies the on-the-wire encoding family used for a group of + * protocol fields. Codes 1–11 ("common" types) fit in a single nibble and + * share a compact one-byte field-ID prefix. Codes 16+ ("uncommon" types) + * require an extra byte for the type nibble. Codes 12–13 are reserved gaps. + * Codes 10001–10004 are top-level container types (`STI_TRANSACTION`, etc.) + * that cannot be embedded inside other serialized objects. + * + * The enum and the companion string-to-int map `kS_TYPE_MAP` are both + * generated from a single `XMACRO` expansion — adding a new type requires + * only one line in the macro. + * + * @note These numeric values are protocol-stable: changing them would break + * binary serialization compatibility with existing ledger data and peers. + */ #pragma push_macro("XMACRO") #undef XMACRO @@ -92,6 +116,12 @@ class STCurrency; // NOLINTNEXTLINE(cppcoreguidelines-use-enum-class) enum SerializedTypeID { XMACRO(TO_ENUM) }; +/** String-to-integer map of all `SerializedTypeID` values. + * + * Generated by the same `XMACRO` expansion as `SerializedTypeID`, so the two + * are always in sync. Used to resolve type names arriving as text (e.g. from + * JSON or RPC) to their integer wire codes. + */ static std::map const kS_TYPE_MAP = {XMACRO(TO_MAP)}; #undef XMACRO @@ -102,58 +132,127 @@ static std::map const kS_TYPE_MAP = {XMACRO(TO_MAP)}; #pragma pop_macro("TO_MAP") // NOLINTEND(readability-identifier-naming) -// constexpr +/** Pack a `SerializedTypeID` and per-type index into a single field code. + * + * The resulting integer is the canonical sort key used for deterministic + * binary serialization: the upper 16 bits hold the type family and the lower + * 16 bits hold the field's position within that family. Fields are always + * serialized in ascending `fieldCode` order. + * + * @param id The wire-type family (e.g. `STI_UINT32`). + * @param index The per-type field index (e.g. 4 for `sfSequence`). + * @return The packed field code `(id << 16) | index`. + */ inline int fieldCode(SerializedTypeID id, int index) { return (safeCast(id) << 16) | index; } -// constexpr +/** Pack a raw integer type ID and per-type index into a single field code. + * + * Overload for callers that already have the type as a plain `int` (e.g. + * when deserializing an unknown type from the wire). + * + * @param id The wire-type family as a raw integer. + * @param index The per-type field index. + * @return The packed field code `(id << 16) | index`. + */ inline int fieldCode(int id, int index) { return (id << 16) | index; } -/** Identifies fields. - - Fields are necessary to tag data in signed transactions so that - the binary format of the transaction can be canonicalized. All - SFields are created at compile time. - - Each SField, once constructed, lives until program termination, and there - is only one instance per fieldType/fieldValue pair which serves the - entire application. -*/ +/** Identifies a single named field in XRPL's binary serialization protocol. + * + * Every field that can appear in a transaction, ledger entry, validation, or + * transaction metadata is represented by exactly one `SField` singleton. All + * instances are created at static-initialization time in `SField.cpp` and + * live until program termination; copy, move, and assignment are deleted to + * enforce the singleton guarantee. + * + * Each field carries a packed `fieldCode` (`(SerializedTypeID << 16) | + * fieldValue`) that serves as both the registry key and the canonical + * comparison value for determining binary serialization order. Fields are + * always serialized in ascending `fieldCode` order — required for + * deterministic transaction signing. + * + * Construction is restricted to `SField.cpp` via `PrivateAccessTagT`: the + * tag type is forward-declared public here but defined only in that + * translation unit, so external code can only look up existing fields through + * `getField()`. + * + * @note Debug builds assert that no two fields share the same code or name at + * registration time. Release builds do not check; a duplicate would + * silently shadow the earlier field. + * + * @see TypedField, fieldCode(), SerializedTypeID + */ class SField { public: + /** Never capture this field's value in transaction metadata. */ static constexpr auto kSMD_NEVER = 0x00; - static constexpr auto kSMD_CHANGE_ORIG = 0x01; // original value when it changes - static constexpr auto kSMD_CHANGE_NEW = 0x02; // new value when it changes - static constexpr auto kSMD_DELETE_FINAL = 0x04; // final value when it is deleted - static constexpr auto kSMD_CREATE = 0x08; // value when it's created - static constexpr auto kSMD_ALWAYS = 0x10; // value when node containing it is affected at all - static constexpr auto kSMD_BASE_TEN = 0x20; // value is treated as base 10, overriding behavior - static constexpr auto kSMD_PSEUDO_ACCOUNT = 0x40; // if this field is set in an ACCOUNT_ROOT - // _only_, then it is a pseudo-account - static constexpr auto kSMD_NEEDS_ASSET = 0x80; // This field needs to be associated with an - // asset before it is serialized as a ledger - // object. Intended for STNumber. + /** Capture the original value when the field changes. */ + static constexpr auto kSMD_CHANGE_ORIG = 0x01; + /** Capture the new value when the field changes. */ + static constexpr auto kSMD_CHANGE_NEW = 0x02; + /** Capture the final value when the enclosing object is deleted. */ + static constexpr auto kSMD_DELETE_FINAL = 0x04; + /** Capture the value when the enclosing object is first created. */ + static constexpr auto kSMD_CREATE = 0x08; + /** Capture the value whenever the enclosing ledger node is touched, + * regardless of whether the field itself changed (used by `sfRootIndex`). */ + static constexpr auto kSMD_ALWAYS = 0x10; + /** Display the value in base-10 rather than hex in JSON metadata + * (used by MPT amount fields such as `sfMaximumAmount`). */ + static constexpr auto kSMD_BASE_TEN = 0x20; + /** The field holds a 256-bit hash that identifies a pseudo-account + * (AMM, Vault, LoanBroker). Used by `sfAMMID`, `sfVaultID`, + * `sfLoanBrokerID`. */ + static constexpr auto kSMD_PSEUDO_ACCOUNT = 0x40; + /** The field is an `STNumber` that must have `associateAsset()` called + * before the enclosing ledger object is serialized. The association + * rounds the value to the asset's precision and removes it if it becomes + * zero (pairs with `kSMD_DEFAULT`). */ + static constexpr auto kSMD_NEEDS_ASSET = 0x80; + /** Default metadata flags: record original value, new value, deletion + * value, and creation value (`kSMD_CHANGE_ORIG | kSMD_CHANGE_NEW | + * kSMD_DELETE_FINAL | kSMD_CREATE`). */ static constexpr auto kSMD_DEFAULT = kSMD_CHANGE_ORIG | kSMD_CHANGE_NEW | kSMD_DELETE_FINAL | kSMD_CREATE; + /** Controls whether a field is included in a transaction's signing payload. + * + * Fields that carry signatures (`sfTxnSignature`, `sfSigners`, + * `sfMasterSignature`, `sfSignature`, `sfCounterpartySignature`) are + * marked `No` to prevent the bootstrap paradox of a signature covering + * itself. + */ enum class IsSigning : unsigned char { No, Yes }; + + /** Convenience constant for the non-signing value. */ static IsSigning const kNOT_SIGNING = IsSigning::No; - int const fieldCodeMem; // (type<<16)|index // TODO: rename, clashes with function - SerializedTypeID const fieldType; // STI_* - int const fieldValue; // Code number for protocol + /** Packed field code: `(SerializedTypeID << 16) | fieldValue`. + * This is the canonical sort key for binary serialization order. + * Sentinel values: -1 for `kSF_INVALID`, 0 for `kSF_GENERIC`. */ + int const fieldCodeMem; + /** Wire-type family for this field (e.g. `STI_UINT32`). */ + SerializedTypeID const fieldType; + /** Per-type field index. Values < 256 are binary-serializable; + * values > 256 are JSON-only (discardable). */ + int const fieldValue; + /** Human-readable field name without the `sf` prefix (e.g. `"Sequence"`). */ std::string const fieldName; + /** Bitmask of `kSMD_*` flags controlling transaction metadata capture. */ int const fieldMeta; + /** Monotonically increasing registration ordinal (1-based). */ int const fieldNum; + /** Whether this field is included in the signing payload. */ IsSigning const signingField; + /** JSON key for this field as a `StaticString` (pointer-stable). */ json::StaticString const jsonName; SField(SField const&) = delete; @@ -164,9 +263,29 @@ public: operator=(SField&&) = delete; public: - struct PrivateAccessTagT; // public, but still an implementation detail + /** Construction access guard — public type, private definition. + * + * Forward-declared here so the constructor signatures are visible, but + * the struct body (and its constructor) is defined only in `SField.cpp`. + * Consequently, only `SField.cpp` can construct `SField` instances. + */ + struct PrivateAccessTagT; - // These constructors can only be called from SField.cpp + /** Construct a typed, named protocol field and register it globally. + * + * Computes `fieldCode = (tid << 16) | fv` and inserts this field into + * the `knownCodeToField` and `knownNameToField` lookup tables. Only + * callable from `SField.cpp` (enforced by `PrivateAccessTagT`). + * + * @param tid Serialized type family (e.g. `STI_UINT32`). + * @param fv Per-type field index; must be < 256 to be + * binary-serializable. + * @param fn Human-readable field name (`sf` prefix already stripped + * by the calling macro). + * @param meta Bitmask of `kSMD_*` flags; defaults to `kSMD_DEFAULT`. + * @param signing Whether this field appears in signing payloads; defaults + * to `IsSigning::Yes`. + */ SField( PrivateAccessTagT, SerializedTypeID tid, @@ -174,118 +293,224 @@ public: char const* fn, int meta = kSMD_DEFAULT, IsSigning signing = IsSigning::Yes); + + /** Construct a special-purpose field from a raw field code. + * + * Used only for the four historical outlier fields (`kSF_INVALID`, + * `kSF_GENERIC`, `kSF_HASH`, `kSF_INDEX`) whose codes cannot be derived + * from the standard `(tid << 16) | fv` formula. Sets `fieldType` to + * `STI_UNKNOWN` and `fieldMeta` to `kSMD_NEVER`. + * + * @param fc Raw field code; -1 for `kSF_INVALID`, 0 for `kSF_GENERIC`. + * @param fn Human-readable field name. + */ explicit SField(PrivateAccessTagT, int fc, char const* fn); + /** Look up a registered field by its packed field code. + * + * @param fieldCode Packed code `(SerializedTypeID << 16) | fieldValue`. + * @return The matching `SField`, or `kSF_INVALID` if none is registered + * with that code. + */ static SField const& getField(int fieldCode); + + /** Look up a registered field by its human-readable name. + * + * Names are stored without the `sf` prefix (e.g. `"Sequence"` not + * `"sfSequence"`). + * + * @param fieldName The name to search for (no `sf` prefix). + * @return The matching `SField`, or `kSF_INVALID` if none is registered + * with that name. + */ static SField const& getField(std::string const& fieldName); + + /** Look up a registered field by raw integer type ID and field index. + * + * @param type Wire-type family as a raw integer. + * @param value Per-type field index. + * @return The matching `SField`, or `kSF_INVALID` if not found. + */ static SField const& getField(int type, int value) { return getField(fieldCode(type, value)); } + /** Look up a registered field by `SerializedTypeID` and field index. + * + * @param type Wire-type family. + * @param value Per-type field index. + * @return The matching `SField`, or `kSF_INVALID` if not found. + */ static SField const& getField(SerializedTypeID type, int value) { return getField(fieldCode(type, value)); } + /** Return the human-readable field name (without the `sf` prefix). */ [[nodiscard]] std::string const& getName() const { return fieldName; } + /** Return true if this field has a meaningful name and positive field code. + * + * Returns false for `kSF_INVALID` (`fieldCode == -1`) and `kSF_GENERIC` + * (`fieldCode == 0`). + */ [[nodiscard]] bool hasName() const { return fieldCodeMem > 0; } + /** Return the JSON key for this field as a pointer-stable `StaticString`. */ [[nodiscard]] json::StaticString const& getJsonName() const { return jsonName; } + /** Implicit conversion to `json::StaticString` for use as a JSON key. */ operator json::StaticString const&() const { return jsonName; } + /** Return true if this field is the `kSF_INVALID` sentinel (`fieldCode == -1`). + * + * `getField()` returns `kSF_INVALID` on a lookup miss. + */ [[nodiscard]] bool isInvalid() const { return fieldCodeMem == -1; } + /** Return true if this field has a positive field code and can carry data. + * + * Equivalent to `!isInvalid() && hasName()`; false for `kSF_INVALID` and + * `kSF_GENERIC`. + */ [[nodiscard]] bool isUseful() const { return fieldCodeMem > 0; } + /** Return true if this field can be round-tripped through binary serialization. + * + * A field is binary-serializable when `fieldValue < 256`. Fields with + * `fieldValue >= 256` (e.g. `kSF_HASH`, `kSF_INDEX`) exist only in JSON + * representations and are excluded from binary encoding. + */ [[nodiscard]] bool isBinary() const { return fieldValue < 256; } - // A discardable field is one that cannot be serialized, and - // should be discarded during serialization,like 'hash'. - // You cannot serialize an object's hash inside that object, - // but you can have it in the JSON representation. + /** Return true if this field must be silently dropped during binary serialization. + * + * Discardable fields (e.g. `sfHash`, `sfIndex`) have `fieldValue > 256` + * and exist only in the JSON form of an object. A round-trip through + * binary will lose them. + */ [[nodiscard]] bool isDiscardable() const { return fieldValue > 256; } + /** Return the packed field code `(SerializedTypeID << 16) | fieldValue`. */ [[nodiscard]] int getCode() const { return fieldCodeMem; } + + /** Return the 1-based registration ordinal assigned at static-init time. */ [[nodiscard]] int getNum() const { return fieldNum; } + + /** Return the total number of `SField` instances registered so far. */ static int getNumFields() { return num; } + /** Return true if any of the bits in `c` are set in this field's metadata mask. + * + * @param c A bitmask of one or more `kSMD_*` constants. + */ [[nodiscard]] bool shouldMeta(int c) const { return (fieldMeta & c) != 0; } + /** Return true if this field should be included in a serialization pass. + * + * A field is included when it is binary-serializable (`fieldValue < 256`) + * and either the caller wants all fields (`withSigningField == true`) or + * this field is marked `IsSigning::Yes`. Passing `withSigningField == + * false` excludes non-signing fields (used when building the signing + * payload for a transaction). + * + * @param withSigningField If false, fields marked `IsSigning::No` are + * excluded. + */ [[nodiscard]] bool shouldInclude(bool withSigningField) const { return (fieldValue < 256) && (withSigningField || (signingField == IsSigning::Yes)); } + /** Equality based on packed field code. */ bool operator==(SField const& f) const { return fieldCodeMem == f.fieldCodeMem; } + /** Inequality based on packed field code. */ bool operator!=(SField const& f) const { return fieldCodeMem != f.fieldCodeMem; } + /** Compare two fields by canonical binary-serialization order. + * + * Fields are ordered by `fieldCode = (SerializedTypeID << 16) | + * fieldValue`, sorting first by wire-type family and then by per-type + * index — matching the canonical XRPL binary format required for + * deterministic transaction signing. + * + * @param f1 First field. + * @param f2 Second field. + * @return -1 if `f1` precedes `f2`, 1 if `f1` follows `f2`, or 0 if + * the comparison is illegal because either field has a non-positive + * code (`kSF_INVALID` or `kSF_GENERIC`). + */ static int compare(SField const& f1, SField const& f2); + /** Return a read-only reference to the global code-to-field registry. + * + * The map key is the packed field code `(SerializedTypeID << 16) | + * fieldValue`. Intended for diagnostic and introspection use only; + * prefer `getField()` for ordinary lookups. + */ static std::unordered_map const& getKnownCodeToField() { @@ -298,7 +523,21 @@ private: static std::unordered_map knownNameToField; }; -/** A field with a type known at compile time. */ +/** An `SField` whose associated C++ type is known at compile time. + * + * Extends `SField` with a `type` alias so callers can statically verify that + * a field is read or written with the correct serialized C++ type. For + * example, `SF_UINT32` is `TypedField>`, making it a + * compile error to read it as an `STAmount`. + * + * All `TypedField` instances are singletons constructed in `SField.cpp`; + * external code cannot create new instances. + * + * @tparam T The serialized C++ type for this field (e.g. `STAmount`, + * `STInteger`). + * + * @see OptionaledField, operator~ + */ template struct TypedField : SField { @@ -308,7 +547,16 @@ struct TypedField : SField explicit TypedField(PrivateAccessTagT pat, Args&&... args); }; -/** Indicate std::optional field semantics. */ +/** Wrapper indicating that a `TypedField` may be absent in a given object. + * + * Obtained via `operator~(TypedField const&)`. The `STObject` proxy + * access pattern uses this to return `std::optional` instead of throwing + * when the field is missing. + * + * @tparam T The serialized C++ type of the underlying field. + * + * @see operator~ + */ template struct OptionaledField { @@ -319,6 +567,15 @@ struct OptionaledField } }; +/** Construct an `OptionaledField` from a `TypedField`, expressing optional semantics. + * + * Allows callers to write `~sfAmount` instead of `OptionaledField(sfAmount)`. + * The resulting value is used with the `STObject` proxy access API to obtain + * an `std::optional` that is empty when the field is absent. + * + * @param f The typed field to treat as optional. + * @return An `OptionaledField` wrapping `f`. + */ template inline OptionaledField operator~(TypedField const& f) @@ -328,13 +585,18 @@ operator~(TypedField const& f) //------------------------------------------------------------------------------ -//------------------------------------------------------------------------------ - -using SF_UINT8 = TypedField>; -using SF_UINT16 = TypedField>; -using SF_UINT32 = TypedField>; -using SF_UINT64 = TypedField>; -using SF_UINT96 = TypedField>; +/** @defgroup SFieldTypeAliases Typed SField aliases + * Convenience type aliases pairing each `SerializedTypeID` wire family with + * its C++ serialized type. Use these as the type of `extern` field + * declarations so that the field carries full type information at compile + * time. + * @{ + */ +using SF_UINT8 = TypedField>; +using SF_UINT16 = TypedField>; +using SF_UINT32 = TypedField>; +using SF_UINT64 = TypedField>; +using SF_UINT96 = TypedField>; using SF_UINT128 = TypedField>; using SF_UINT160 = TypedField>; using SF_UINT192 = TypedField>; @@ -342,17 +604,18 @@ using SF_UINT256 = TypedField>; using SF_UINT384 = TypedField>; using SF_UINT512 = TypedField>; -using SF_INT32 = TypedField>; -using SF_INT64 = TypedField>; +using SF_INT32 = TypedField>; +using SF_INT64 = TypedField>; -using SF_ACCOUNT = TypedField; -using SF_AMOUNT = TypedField; -using SF_ISSUE = TypedField; -using SF_CURRENCY = TypedField; -using SF_NUMBER = TypedField; -using SF_VL = TypedField; -using SF_VECTOR256 = TypedField; +using SF_ACCOUNT = TypedField; +using SF_AMOUNT = TypedField; +using SF_ISSUE = TypedField; +using SF_CURRENCY = TypedField; +using SF_NUMBER = TypedField; +using SF_VL = TypedField; +using SF_VECTOR256 = TypedField; using SF_XCHAIN_BRIDGE = TypedField; +/** @} */ //------------------------------------------------------------------------------ @@ -365,7 +628,19 @@ using SF_XCHAIN_BRIDGE = TypedField; #define UNTYPED_SFIELD(sfName, stiSuffix, fieldValue, ...) extern SField const sfName; #define TYPED_SFIELD(sfName, stiSuffix, fieldValue, ...) extern SF_##stiSuffix const sfName; +/** Sentinel returned by `SField::getField()` on a lookup miss. + * + * `fieldCode == -1`; `isInvalid()` returns true. Callers that receive this + * value should treat the requested field as unrecognized. + */ extern SField const kSF_INVALID; + +/** Catch-all field for untyped serialization contexts. + * + * `fieldCode == 0`; `isUseful()` and `hasName()` return false. Used + * internally when a context requires an `SField` reference but no specific + * field is applicable. + */ extern SField const kSF_GENERIC; #include diff --git a/include/xrpl/protocol/SOTemplate.h b/include/xrpl/protocol/SOTemplate.h index 72e0573d29..46546836e8 100644 --- a/include/xrpl/protocol/SOTemplate.h +++ b/include/xrpl/protocol/SOTemplate.h @@ -1,3 +1,13 @@ +/** @file + * Schema definitions for XRPL serialized objects. + * + * Provides `SOElement` (a single field's schema entry) and `SOTemplate` (the + * complete ordered schema for one transaction, ledger entry, or inner object + * type). Templates are constructed once at startup by the `KnownFormats` + * singletons and are thereafter read-only, enabling lock-free O(1) field + * lookup during every serialization and deserialization call. + */ + #pragma once #include @@ -10,26 +20,70 @@ namespace xrpl { -/** Kind of element in each entry of an SOTemplate. */ +/** Field-presence semantics for a single entry in an `SOTemplate`. + * + * Controls how `STObject` treats a field during deserialization, validation, + * and serialization: + * + * - `SoeRequired` — the field must be present; absence is a fatal error. + * - `SoeOptional` — the field may be absent; if present it may carry the + * type's default value (presence with default has distinct protocol meaning). + * - `SoeDefault` — the field may be absent; if present it must NOT carry the + * type's default value. Inner objects that contain `SoeDefault` fields must + * be created via `STObject::makeInnerObject()` to preserve this invariant. + * - `SoeInvalid` — sentinel returned by `STObject::getFieldStyle()` when the + * object has no associated template; never used in a live schema. + * + * @note `SoeOptional` and `SoeDefault` are subtly different: for some fields + * (e.g., `QualityIn` on a trust line) having the field present with its + * default value and having it absent carry different protocol semantics. + * Use `SoeDefault` when the field must not encode redundant default state. + */ // 2026 usages, 129 files // NOLINTNEXTLINE(cppcoreguidelines-use-enum-class) enum SOEStyle { SoeInvalid = -1, - SoeRequired = 0, // required - SoeOptional = 1, // optional, may be present with default value - SoeDefault = 2, // optional, if present, must not have default value - // inner object with the default fields has to be - // constructed with STObject::makeInnerObject() + SoeRequired = 0, ///< Field must be present. + SoeOptional = 1, ///< Field may be absent; if present, may hold default value. + SoeDefault = 2, ///< Field may be absent; if present, must not hold default value. }; -// Part of a Python-parsed DSL (transactions.macro); bare enumerator names required by the parser -/** Amount fields that can support MPT */ +/** Multi-Purpose Token (MPT) awareness annotation for amount and issue fields. + * + * Applied only to `STAmount` and `STIssue` typed fields (enforced by the + * constrained `SOElement` constructor). Allows the validation layer in + * `STObject` and `STTx` to check MPT compatibility at the schema level rather + * than in scattered per-transaction code. + * + * - `SoeMptNone` — field does not carry an amount or issue; MPT check + * is never performed. Default for all non-amount fields. + * - `SoeMptSupported` — the transaction format allows MPT in this field. + * - `SoeMptNotSupported` — the transaction format explicitly forbids MPT in + * this field; validation rejects any MPT value. + * + * @note Bare enumerator names (without a class scope) are required because + * these values are parsed by the Python DSL that processes + * `transactions.macro`. + */ // NOLINTNEXTLINE(cppcoreguidelines-use-enum-class) enum SOETxMPTIssue { SoeMptNone, SoeMptSupported, SoeMptNotSupported }; //------------------------------------------------------------------------------ -/** An element in a SOTemplate. */ +/** One field's schema entry inside an `SOTemplate`. + * + * Pairs an `SField` reference with its `SOEStyle` presence semantics and, + * for amount/issue fields, an `SOETxMPTIssue` MPT-awareness tag. + * + * `SField` instances are immovable, non-copyable process-lifetime singletons. + * Storing a `std::reference_wrapper` rather than a raw pointer communicates + * the non-owning relationship clearly and allows `SOElement` to be held in a + * `std::vector` (which requires copyable/movable elements). + * + * @note Both constructors call the private `init()` helper, which throws if + * the field is not "useful" (i.e., `fieldCode <= 0`, as for `sfInvalid` + * or `sfGeneric`). This catches schema bugs at application startup. + */ class SOElement { // Use std::reference_wrapper so SOElement can be stored in a std::vector. @@ -38,6 +92,12 @@ class SOElement SOETxMPTIssue supportMpt_ = SoeMptNone; private: + /** Validate that the wrapped field is a known, named, serializable field. + * + * @param fieldName The field to validate. + * @throws std::runtime_error if `fieldName.isUseful()` returns false + * (i.e., `fieldCode <= 0`), indicating a sentinel or placeholder field. + */ void init(SField const& fieldName) const { @@ -51,11 +111,31 @@ private: } public: + /** Construct a schema entry for any serializable field. + * + * @param fieldName The field this entry describes; must satisfy + * `isUseful()` (positive field code). + * @param style Presence semantics: required, optional, or default. + * @throws std::runtime_error if @p fieldName is not a useful field. + */ SOElement(SField const& fieldName, SOEStyle style) : sField_(fieldName), style_(style) { init(fieldName); } + /** Construct a schema entry for an `STAmount` or `STIssue` field with MPT annotation. + * + * The `requires` constraint restricts this overload to `STAmount` and + * `STIssue` typed fields, enforcing that MPT support annotations can only + * appear on fields that actually carry amounts or asset specifiers. + * + * @tparam T Must be `STAmount` or `STIssue`. + * @param fieldName The typed amount or issue field this entry describes. + * @param style Presence semantics: required, optional, or default. + * @param supportMpt Whether this field accepts MPT values. Defaults to + * `SoeMptNotSupported` so new amount fields must explicitly opt in. + * @throws std::runtime_error if @p fieldName is not a useful field. + */ template requires(std::is_same_v || std::is_same_v) SOElement( @@ -67,18 +147,26 @@ public: init(fieldName); } + /** Return the `SField` this entry describes. */ [[nodiscard]] SField const& sField() const { return sField_.get(); } + /** Return the field's presence semantics within its containing object type. */ [[nodiscard]] SOEStyle style() const { return style_; } + /** Return the MPT-awareness annotation for this amount or issue field. + * + * @note Returns `SoeMptNone` for all non-amount, non-issue fields; callers + * should only interpret the result when the field type is `STAmount` + * or `STIssue`. + */ [[nodiscard]] SOETxMPTIssue supportMPT() const { @@ -88,67 +176,125 @@ public: //------------------------------------------------------------------------------ -/** Defines the fields and their attributes within a STObject. - Each subclass of SerializedObject will provide its own template - describing the available fields and their metadata attributes. -*/ +/** Immutable field schema for one serialized object type in the XRP Ledger. + * + * Holds the ordered list of `SOElement` entries for a single transaction, + * ledger entry, or inner object type, together with a dense reverse-lookup + * table that maps `SField::getNum()` to the element's position in O(1). + * + * Templates are constructed once at process startup by `KnownFormats` + * subclasses (`TxFormats`, `LedgerFormats`, `InnerObjectFormats`) and are + * thereafter immutable. All consumers hold a `const*` or `const&`; no + * copying is ever required. Consequently the copy constructor and copy + * assignment operator are deleted — the type is move-only. + * + * @note The constructor snapshots `SField::getNumFields()` to size the index + * table. Fields registered after the template is constructed cannot be + * looked up and will cause `getIndex()` to throw. In practice this is + * never an issue because all `SField` singletons are registered before + * `main()` runs, ahead of the `KnownFormats` singletons. + * + * @see SOElement, SOEStyle, STObject::applyTemplate(), STObject::set() + */ class SOTemplate { public: + SOTemplate(SOTemplate const&) = delete; + SOTemplate& + operator=(SOTemplate const&) = delete; + // Copying vectors is expensive. Make this a move-only type until // there is motivation to change that. SOTemplate(SOTemplate&& other) = default; SOTemplate& operator=(SOTemplate&& other) = default; - /** Create a template populated with all fields. - After creating the template fields cannot be added, modified, or removed. - */ + /** Build the schema from a type-specific and a shared field list. + * + * Concatenates @p uniqueFields followed by @p commonFields into a single + * ordered element sequence, then constructs the O(1) index table. + * + * @param uniqueFields Fields specific to this object type; placed first in + * the element sequence. + * @param commonFields Fields shared across all object types of this kind + * (e.g., `Fee`, `Sequence`, `SigningPubKey` for transactions); appended + * after unique fields. + * @throws std::runtime_error if any field has an out-of-range field number + * or appears more than once across both lists. + */ SOTemplate(std::vector uniqueFields, std::vector commonFields = {}); - /** Create a template populated with all fields. - Note: Defers to the vector constructor above. - */ + /** Convenience overload accepting initializer lists; delegates to the vector constructor. + * + * @param uniqueFields Fields specific to this object type. + * @param commonFields Fields shared across all object types of this kind. + * @throws std::runtime_error forwarded from the vector constructor. + */ SOTemplate( std::initializer_list uniqueFields, std::initializer_list commonFields = {}); - /* Provide for the enumeration of fields */ + /** Return an iterator to the first `SOElement` in the schema. */ [[nodiscard]] std::vector::const_iterator begin() const { return elements_.cbegin(); } + /** Return an iterator to the first `SOElement` in the schema. */ [[nodiscard]] std::vector::const_iterator cbegin() const { return begin(); } + /** Return a past-the-end iterator for the element sequence. */ [[nodiscard]] std::vector::const_iterator end() const { return elements_.cend(); } + /** Return a past-the-end iterator for the element sequence. */ [[nodiscard]] std::vector::const_iterator cend() const { return end(); } - /** The number of entries in this template */ + /** Return the number of field entries in this schema. */ [[nodiscard]] std::size_t size() const { return elements_.size(); } - /** Retrieve the position of a named field. */ + /** Return the position of @p sField in the element sequence, or -1 if absent. + * + * Uses a direct array subscript into the internal index table for O(1) + * cost. This is the hot path called on every field access during + * serialization and deserialization. + * + * @param sField The field to look up. + * @return Index into the element sequence, or -1 if the field is not part + * of this schema. + * @throws std::runtime_error if @p sField has a non-positive or + * out-of-range field number (i.e., a sentinel field or one registered + * after this template was constructed). + */ [[nodiscard]] int getIndex(SField const&) const; + /** Return the presence-style of @p sf within this schema. + * + * @param sf The field whose style to retrieve; must be present in this + * template (i.e., `getIndex(sf) != -1`). + * @return The `SOEStyle` declared for this field in the schema. + * @note Calling this with a field that is not in the template results in + * undefined behavior (out-of-bounds array access via the `-1` sentinel + * returned by `getIndex()`). Use `getIndex()` to check presence first + * when the field may be absent. + */ [[nodiscard]] SOEStyle style(SField const& sf) const { @@ -157,7 +303,7 @@ public: private: std::vector elements_; - std::vector indices_; // field num -> index + std::vector indices_; ///< Dense lookup table: field num -> index into elements_. }; } // namespace xrpl diff --git a/include/xrpl/protocol/STAccount.h b/include/xrpl/protocol/STAccount.h index 65f404d58d..949776f7da 100644 --- a/include/xrpl/protocol/STAccount.h +++ b/include/xrpl/protocol/STAccount.h @@ -1,5 +1,15 @@ #pragma once +/** @file + * Defines `STAccount`, the serialized-type wrapper for 160-bit XRPL account + * identifiers used inside transactions and ledger objects. + * + * The internal storage is a plain `AccountID` (`base_uint<160>`) — no heap + * allocation — while the wire format deliberately preserves the + * variable-length (VL) blob encoding of the original `STBlob`-based + * implementation for byte-for-byte ledger compatibility. + */ + #include #include #include @@ -8,10 +18,28 @@ namespace xrpl { +/** Serialized-type wrapper for a 160-bit XRPL account identifier. + * + * `STAccount` stores an `AccountID` value in a fixed-size `uint160` (no + * heap allocation) while serializing and deserializing using the + * VL-prefixed blob encoding of the legacy `STBlob` implementation, keeping + * the wire format byte-for-byte compatible with all existing ledger data. + * + * A `bool default_` flag tracks whether the field has ever been explicitly + * assigned. A default field serializes as a zero-length VL blob, which is + * distinct from a field explicitly set to the all-zeros pseudo-account + * (`noAccount()`). Any call to `setValue()` or `operator=` clears the flag, + * even when the assigned value is zero. + * + * Inherits `CountedObject` for lock-free diagnostic instance + * counting, and is `final` — no further derivation is expected. + * + * @see STBase, CountedObject + */ class STAccount final : public STBase, public CountedObject { private: - // The original implementation of STAccount kept the value in an STBlob. + // The original implementation kept the value in an STBlob. // But an STAccount is always 160 bits, so we can store it with less // overhead in an xrpl::uint160. However, so the serialized format of the // STAccount stays unchanged, we serialize and deserialize like an STBlob. @@ -21,40 +49,154 @@ private: public: using value_type = AccountID; + /** Construct an anonymous, unset account field. + * + * Sets the stored value to zero and marks the field as default (unset). + * A default field serializes as a zero-length VL blob and returns an + * empty string from `getText()`. + */ STAccount(); + /** Construct a named but unset account field. + * + * Binds the field to `n` but leaves it in the default (unset) state. + * Typical use: pre-populating an `STObject` slot before the account + * address is known. + * + * @param n The `SField` descriptor identifying this field (e.g. `sfAccount`). + */ STAccount(SField const& n); + + /** Construct from a raw VL-blob byte buffer. + * + * An empty buffer is the canonical round-trip encoding of a default + * (unset) field and leaves the object in the default state. A non-empty + * buffer must be exactly 20 bytes; any other size throws. + * + * @param n The `SField` descriptor for this field. + * @param v Raw bytes from a VL-blob read. Must be empty or exactly 20 bytes. + * @throws std::runtime_error if `v` is non-empty and not exactly 20 bytes. + */ STAccount(SField const& n, Buffer const& v); + + /** Deserialize an account field from a wire-format byte stream. + * + * Extracts the next VL-prefixed blob from `sit` and delegates to the + * `Buffer` constructor for size validation and value assignment. + * + * @param sit Forward cursor over the serialized byte stream; advanced + * past the VL blob on return. + * @param name The `SField` descriptor for this field. + * @throws std::runtime_error if the extracted blob is not empty or 20 bytes. + */ STAccount(SerialIter& sit, SField const& name); + + /** Construct from a known `AccountID` value. + * + * Marks the field as non-default regardless of whether `v` is the + * zero account. This is the standard path when the account address + * is already available at construction time. + * + * @param n The `SField` descriptor for this field. + * @param v The 160-bit account identifier to store. + */ STAccount(SField const& n, AccountID const& v); + /** Return the `SerializedTypeID` constant for this type (`STI_ACCOUNT`). */ [[nodiscard]] SerializedTypeID getSType() const override; + /** Return the account address as a Base58Check string, or empty if unset. + * + * A default (unset) field returns `""` rather than the Base58 encoding + * of the all-zeros pseudo-account, preserving the distinction between an + * unset field and one explicitly set to `noAccount()`. + * + * @return Base58Check-encoded address, or `""` when `isDefault()` is true. + */ [[nodiscard]] std::string getText() const override; + /** Append this field to `s` using VL-blob wire encoding. + * + * A default (unset) field serializes as a zero-length VL blob (one + * `0x00` byte on the wire). A non-default field serializes as a 20-byte + * VL blob. This preserves byte-for-byte compatibility with the legacy + * `STBlob`-based encoding and distinguishes "unset" from "explicitly set + * to the zero account." + * + * @param s The `Serializer` to append to. + * @note Asserts (debug builds only) that the associated `SField` is a + * binary field of type `STI_ACCOUNT`. + */ void add(Serializer& s) const override; + /** Check semantic equivalence with another serialized field. + * + * Two `STAccount` objects are equivalent only when both their `default_` + * flags and their 160-bit values agree. The `SField` name is ignored — + * equivalence is purely about stored account state, not which field slot + * the object occupies. + * + * @param t The field to compare against. + * @return `true` if `t` is an `STAccount` with the same default flag and + * value; `false` if `t` is a different type or either attribute differs. + * @note Callers that need to compare only the address (ignoring default + * state) should use `operator==` on the `value()` accessors directly. + */ [[nodiscard]] bool isEquivalent(STBase const& t) const override; + /** Return `true` if this field has never been explicitly assigned. + * + * A default field serializes as a zero-length VL blob. Assigning any + * `AccountID` — including the zero account — clears the default flag. + */ [[nodiscard]] bool isDefault() const override; + /** Assign an `AccountID` value, clearing the default flag. + * + * @param value The account identifier to store. + * @return `*this`, to support chained assignments. + */ STAccount& operator=(AccountID const& value); + /** Return the stored 160-bit account identifier. + * + * Returns the underlying `AccountID` regardless of whether the field is + * in the default state. Callers that need to distinguish "unset" from + * a real zero account should check `isDefault()` first. + */ [[nodiscard]] AccountID const& value() const noexcept; + /** Store `v` and mark this field as explicitly set. + * + * Unconditionally clears the default flag, even when `v` is the zero + * account, so that `isDefault()` returns `false` after any call. + * + * @param v The 160-bit account identifier to store. + */ void setValue(AccountID const& v); private: + /** Place a copy of this object into `buf` (if it fits within `n` bytes) + * or heap-allocate a copy via `STBase::emplace()`. + * + * Used by `detail::STVar` for the small-object optimization. + */ STBase* copy(std::size_t n, void* buf) const override; + + /** Place a moved instance into `buf` (if it fits within `n` bytes) + * or heap-allocate via `STBase::emplace()`. + * + * Used by `detail::STVar` for the small-object optimization. + */ STBase* move(std::size_t n, void* buf) override; @@ -81,30 +223,39 @@ STAccount::setValue(AccountID const& v) default_ = false; } +/** Return `true` if both `STAccount` objects hold the same 160-bit value. + * + * @note The default flag is not considered; use `isEquivalent()` when + * "set-ness" must also match. + */ inline bool operator==(STAccount const& lhs, STAccount const& rhs) { return lhs.value() == rhs.value(); } +/** Three-way-comparable less-than for two `STAccount` values. */ inline auto operator<(STAccount const& lhs, STAccount const& rhs) { return lhs.value() < rhs.value(); } +/** Return `true` if the `STAccount` holds the same 160-bit value as `rhs`. */ inline bool operator==(STAccount const& lhs, AccountID const& rhs) { return lhs.value() == rhs; } +/** Less-than comparison between an `STAccount` and a raw `AccountID`. */ inline auto operator<(STAccount const& lhs, AccountID const& rhs) { return lhs.value() < rhs; } +/** Less-than comparison between a raw `AccountID` and an `STAccount`. */ inline auto operator<(AccountID const& lhs, STAccount const& rhs) { diff --git a/include/xrpl/protocol/STAmount.h b/include/xrpl/protocol/STAmount.h index f05d44441d..d6aa261e65 100644 --- a/include/xrpl/protocol/STAmount.h +++ b/include/xrpl/protocol/STAmount.h @@ -1,3 +1,12 @@ +/** @file + * Canonical on-ledger amount type unifying XRP, IOU, and MPT quantities. + * + * `STAmount` is the serializable amount type used throughout the XRP Ledger. + * It stores XRP drops, IOU floating-point amounts, and Multi-Purpose Token + * (MPT) integers behind a single interface that integrates with the ledger's + * typed-field system via `STBase`. + */ + #pragma once #include @@ -16,21 +25,47 @@ namespace xrpl { -// Internal form: -// 1: If amount is zero, then value is zero and offset is -100 -// 2: Otherwise: -// legal offset range is -96 to +80 inclusive -// value range is 10^15 to (10^16 - 1) inclusive -// amount = value * [10 ^ offset] - -// Wire form: -// High 8 bits are (offset+142), legal range is, 80 to 22 inclusive -// Low 56 bits are value, legal range is 10^15 to (10^16 - 1) inclusive +/** Unified serializable amount for XRP, IOU, and MPT assets. + * + * `STAmount` is the canonical on-ledger amount type. It stores three + * fundamentally different quantity kinds — XRP drops, IOU floating-point + * amounts, and Multi-Purpose Token integers — behind a single interface + * that integrates with the ledger's typed-field system via `STBase`. + * + * ## Internal representation + * + * For **IOU** amounts the value is stored as normalized scientific notation: + * `amount = value × 10^offset`. The mantissa is in `[kMIN_VALUE, kMAX_VALUE]` + * i.e. `[10^15, 10^16 − 1]`, and the exponent is in `[kMIN_OFFSET, kMAX_OFFSET]` + * i.e. `[-96, +80]`. Zero is encoded as `value = 0, offset = −100`; the + * sentinel −100 ensures that zero sorts below every positive IOU with a + * large-negative exponent. + * + * For **XRP and MPT** (`integral()` types) `offset` is always 0 and `value` + * directly holds the raw drop or token count. XRP is bounded by `kMAX_NATIVE_N` + * (10^17 drops); MPT is bounded by `INT64_MAX`. + * + * ## Wire encoding + * + * Amounts are serialised into a packed 64-bit word: + * - Bit 63 = 0 → native (XRP or MPT); bit 61 further distinguishes them. + * - Bit 63 = 1 → issued currency (IOU). + * - Bit 62 = sign (1 = positive). + * - For IOU: bits 55–62 = `offset + 97`; bits 0–53 = mantissa. + * + * @note `canonicalize()` normalises the mantissa into `[kMIN_VALUE, kMAX_VALUE]` + * on every checked construction path. Constructors tagged `Unchecked` skip + * this step and require the caller to guarantee the representation is + * already canonical. + */ class STAmount final : public STBase, public CountedObject { public: + /** Unsigned integer type used to store the IOU mantissa or integral amount value. */ using mantissa_type = std::uint64_t; + /** Signed integer type used to store the IOU base-10 exponent. */ using exponent_type = int; + /** Pair of (mantissa, exponent) for use in serialization and arithmetic helpers. */ using rep = std::pair; private: @@ -42,34 +77,82 @@ private: public: using value_type = STAmount; + /** Minimum legal IOU exponent (offset). Zero and integral types always use 0. */ constexpr static int kMIN_OFFSET = -96; + /** Maximum legal IOU exponent (offset). */ constexpr static int kMAX_OFFSET = 80; - // Maximum native value supported by the code + /** Minimum normalized IOU mantissa (10^15). Mantissas below this are scaled up. */ constexpr static std::uint64_t kMIN_VALUE = 1'000'000'000'000'000ull; static_assert(isPowerOfTen(kMIN_VALUE)); + /** Maximum normalized IOU mantissa (10^16 − 1). Mantissas above this are scaled down. */ constexpr static std::uint64_t kMAX_VALUE = (kMIN_VALUE * 10) - 1; static_assert(kMAX_VALUE == 9'999'999'999'999'999ull); + /** Absolute maximum XRP/MPT value that the code will store internally + * (9 × 10^18 drops). Enforcement happens in the wire decoder and + * network-validity check (@ref isLegalNet). */ constexpr static std::uint64_t kMAX_NATIVE = 9'000'000'000'000'000'000ull; - // Max native value on network. + /** Maximum XRP drop value permitted on the network (10^17 = 100 billion XRP). + * Validated by @ref isLegalNet; amounts above this are consensus-invalid. */ constexpr static std::uint64_t kMAX_NATIVE_N = 100'000'000'000'000'000ull; + + // --- Wire-format flag bits (bit 63 is MSB) --- + + /** Wire bit 63: set for IOU amounts, clear for native (XRP or MPT). */ constexpr static std::uint64_t kISSUED_CURRENCY = 0x8'000'000'000'000'000ull; + /** Wire bit 62: sign bit — set means positive. */ constexpr static std::uint64_t kPOSITIVE = 0x4'000'000'000'000'000ull; + /** Wire bit 61: distinguishes MPT (set) from XRP (clear) for native amounts. */ constexpr static std::uint64_t kMP_TOKEN = 0x2'000'000'000'000'000ull; + /** Mask that strips the `kPOSITIVE` and `kMP_TOKEN` flag bits, leaving the + * raw value word for MPT amounts. */ constexpr static std::uint64_t kVALUE_MASK = ~(kPOSITIVE | kMP_TOKEN); + /** Wire encoding of a unit quality offer (rate = 1.0). */ static std::uint64_t const kU_RATE_ONE; //-------------------------------------------------------------------------- + // + // Constructors + // + //-------------------------------------------------------------------------- + + /** Deserialize an STAmount from a byte stream. + * + * Decodes the compact 64-bit wire word plus any trailing currency/issuer + * or MPTID bytes. Throws `std::runtime_error` on malformed input + * (negative zero, mantissa out of range, invalid currency/account). + * + * @param sit Source iterator positioned at the first byte of the amount. + * @param name The SField that names this field in the parent STObject. + */ STAmount(SerialIter& sit, SField const& name); + /** Tag type that bypasses `canonicalize()` on construction. + * + * Use only when the caller can guarantee the representation is already + * in canonical form (e.g. inside arithmetic helpers that maintain + * invariants, or when reading from a known-good source). Prefer the + * checked constructors for all other call sites. + */ struct Unchecked { explicit Unchecked() = default; }; - // Do not call canonicalize + /** Construct a named STAmount with a pre-canonical representation. + * + * Stores `mantissa × 10^exponent` (with sign) verbatim — `canonicalize()` + * is **not** called. The caller must ensure the values satisfy the IOU + * invariants or, for integral assets, that `exponent == 0`. + * + * @param name SField associated with this amount. + * @param asset Asset type (Issue or MPTIssue). + * @param mantissa Raw unsigned mantissa. + * @param exponent Base-10 exponent. + * @param negative True if the amount is negative. + */ template STAmount( SField const& name, @@ -79,6 +162,15 @@ public: bool negative, Unchecked); + /** Construct an anonymous STAmount with a pre-canonical representation. + * + * Anonymous (no SField) variant of the `Unchecked` constructor above. + * + * @param asset Asset type (Issue or MPTIssue). + * @param mantissa Raw unsigned mantissa. + * @param exponent Base-10 exponent. + * @param negative True if the amount is negative. + */ template STAmount( A const& asset, @@ -87,7 +179,18 @@ public: bool negative, Unchecked); - // Call canonicalize + /** Construct a named STAmount, calling `canonicalize()` afterward. + * + * Normalises the mantissa into `[kMIN_VALUE, kMAX_VALUE]` by adjusting + * the exponent. Throws `std::runtime_error` on overflow. Subnormals + * (exponent below `kMIN_OFFSET` after scaling) are silently zeroed. + * + * @param name SField associated with this amount. + * @param asset Asset type (Issue or MPTIssue). + * @param mantissa Unsigned mantissa (defaults to 0 → zero amount). + * @param exponent Base-10 exponent (defaults to 0). + * @param negative True if the amount is negative (defaults to false). + */ template STAmount( SField const& name, @@ -96,14 +199,44 @@ public: exponent_type exponent = 0, bool negative = false); + /** Construct a named XRP amount from a signed 64-bit drop count. + * + * Negative values set the sign flag; the stored mantissa is the absolute value. + * + * @param name SField associated with this amount. + * @param mantissa Signed drop count. + */ STAmount(SField const& name, std::int64_t mantissa); + /** Construct a named XRP amount from an unsigned 64-bit drop count. + * + * @param name SField associated with this amount. + * @param mantissa Unsigned drop count (defaults to 0). + * @param negative True if the amount is negative (defaults to false). + */ STAmount(SField const& name, std::uint64_t mantissa = 0, bool negative = false); + /** Construct an anonymous XRP amount from an unsigned 64-bit drop count. + * + * @param mantissa Unsigned drop count (defaults to 0). + * @param negative True if the amount is negative (defaults to false). + */ explicit STAmount(std::uint64_t mantissa = 0, bool negative = false); + /** Construct a named copy of an existing STAmount, preserving asset and value. + * + * @param name SField to attach to the copy. + * @param amt Source amount. + */ explicit STAmount(SField const& name, STAmount const& amt); + /** Construct an anonymous STAmount with the given asset, calling `canonicalize()`. + * + * @param asset Asset type (Issue or MPTIssue). + * @param mantissa Unsigned mantissa (defaults to 0). + * @param exponent Base-10 exponent (defaults to 0). + * @param negative True if the amount is negative (defaults to false). + */ template STAmount(A const& asset, std::uint64_t mantissa = 0, int exponent = 0, bool negative = false) : asset_(asset), value_(mantissa), offset_(exponent), isNegative_(negative) @@ -111,25 +244,84 @@ public: canonicalize(); } + /** Construct an anonymous STAmount from a 32-bit unsigned mantissa. + * + * Widens to `uint64_t` then delegates to the canonical constructor. + * + * @param asset Asset type (Issue or MPTIssue). + * @param mantissa 32-bit unsigned mantissa. + * @param exponent Base-10 exponent (defaults to 0). + * @param negative True if the amount is negative (defaults to false). + */ // VFALCO Is this needed when we have the previous signature? template STAmount(A const& asset, std::uint32_t mantissa, int exponent = 0, bool negative = false); + /** Construct an anonymous STAmount from a signed 64-bit mantissa. + * + * Negative values set the sign flag; the stored mantissa is the absolute value. + * + * @param asset Asset type (Issue or MPTIssue). + * @param mantissa Signed mantissa; sign extracted via `set()`. + * @param exponent Base-10 exponent (defaults to 0). + */ template STAmount(A const& asset, std::int64_t mantissa, int exponent = 0); + /** Construct an anonymous STAmount from a plain `int` mantissa. + * + * Widens to `int64_t` then delegates to the signed constructor. + * + * @param asset Asset type (Issue or MPTIssue). + * @param mantissa Signed integer mantissa. + * @param exponent Base-10 exponent (defaults to 0). + */ template STAmount(A const& asset, int mantissa, int exponent = 0); + /** Construct an STAmount from a `Number`, rounding to the asset's precision. + * + * Converts the high-precision `Number` into the appropriate internal + * representation. For integral assets (XRP, MPT) the fractional part is + * dropped; for IOU assets the mantissa is normalised into + * `[kMIN_VALUE, kMAX_VALUE]`. + * + * @param asset Asset type (Issue or MPTIssue). + * @param number High-precision value to convert. + */ template STAmount(A const& asset, Number const& number) : STAmount(fromNumber(asset, number)) { } - // Legacy support for new-style amounts + /** Construct from a lean `IOUAmount` and its associated `Issue`. + * + * Bridges from the lightweight `IOUAmount` representation to the + * serializable `STAmount` form. + * + * @param amount Lean IOU amount (mantissa + exponent). + * @param issue Currency/issuer identity for the resulting STAmount. + */ STAmount(IOUAmount const& amount, Issue const& issue); + + /** Construct from a lean `XRPAmount`. + * + * @param amount XRP drop count. + */ STAmount(XRPAmount const& amount); + + /** Construct from a lean `MPTAmount` and its associated `MPTIssue`. + * + * @param amount Lean MPT amount (raw integer token count). + * @param mptIssue MPT issuance identity. + */ STAmount(MPTAmount const& amount, MPTIssue const& mptIssue); + + /** Convert to a high-precision `Number`. + * + * Dispatches via `Asset::visit()` to the appropriate lean extractor + * (`xrp()`, `iou()`, or `mpt()`) and constructs a `Number` from it. + */ operator Number() const; //-------------------------------------------------------------------------- @@ -138,39 +330,83 @@ public: // //-------------------------------------------------------------------------- + /** Return the base-10 exponent. + * + * For IOU amounts this is in `[kMIN_OFFSET, kMAX_OFFSET]`, or −100 when + * the amount is zero. For XRP and MPT amounts this is always 0. + */ [[nodiscard]] int exponent() const noexcept; + /** True if this amount is an integral (non-floating-point) type. + * + * Returns true for both XRP and MPT; false for IOU. Integral types store + * `offset == 0` and a raw integer token count in `value`. + */ [[nodiscard]] bool integral() const noexcept; + /** True if this amount represents native XRP. + * + * Returns false for IOU and MPT amounts. + */ [[nodiscard]] bool native() const noexcept; + /** True if the embedded asset is of type `TIss`. + * + * @tparam TIss Either `Issue` (covers both XRP and IOU) or `MPTIssue`. + */ template [[nodiscard]] constexpr bool holds() const noexcept; + /** True if this amount is negative. + * + * A canonical zero amount is never negative. + */ [[nodiscard]] bool negative() const noexcept; + /** Return the raw unsigned mantissa. + * + * For IOU amounts this is in `[kMIN_VALUE, kMAX_VALUE]` (or 0 for zero). + * For XRP and MPT amounts this is the raw drop or token count. + */ [[nodiscard]] std::uint64_t mantissa() const noexcept; + /** Return the asset (Issue or MPTIssue) carried by this amount. */ [[nodiscard]] Asset const& asset() const; + /** Return the embedded asset as the specific issue type `TIss`. + * + * @tparam TIss Either `Issue` or `MPTIssue`. + * @throws std::logic_error if the asset is not of type `TIss`. + */ template constexpr TIss const& get() const; + /** Mutable variant of `get()`. + * + * @tparam TIss Either `Issue` or `MPTIssue`. + * @throws std::logic_error if the asset is not of type `TIss`. + */ template TIss& get(); + /** Return the issuer account for IOU amounts; `noAccount()` for XRP; + * the MPT issuer account for MPT amounts. */ [[nodiscard]] AccountID const& getIssuer() const; + /** Return the sign as −1, 0, or +1. + * + * A canonical zero always returns 0 regardless of the `negative` flag. + */ [[nodiscard]] int signum() const noexcept; @@ -178,9 +414,16 @@ public: [[nodiscard]] STAmount zeroed() const; + /** Populate a JSON object with the amount's fields (value, currency, issuer / mpt_issuance_id). */ void setJson(json::Value&) const; + /** Returns a const reference to `*this`. + * + * Provided so that `STAmount` satisfies the same `value()` accessor + * pattern as the lean amount types (`XRPAmount`, `IOUAmount`, `MPTAmount`), + * enabling generic template code that calls `.value()` uniformly. + */ [[nodiscard]] STAmount const& value() const noexcept; @@ -190,19 +433,34 @@ public: // //-------------------------------------------------------------------------- + /** True if the amount is non-zero. */ explicit operator bool() const noexcept; + /** Add `rhs` to this amount in place. + * + * @pre Both amounts must have the same asset; mixing asset types is + * undefined behaviour and will produce a wrong result at runtime. + */ STAmount& operator+=(STAmount const&); + + /** Subtract `rhs` from this amount in place. + * + * @pre Both amounts must have the same asset; mixing asset types is + * undefined behaviour and will produce a wrong result at runtime. + */ STAmount& operator-=(STAmount const&); + /** Zero this amount, preserving its asset identity. */ STAmount& operator=(beast::Zero); + /** Assign from a lean `XRPAmount`, preserving the XRP asset identity. */ STAmount& operator=(XRPAmount const& amount); + /** Assign from a `Number`, rounding to the current asset's precision. */ STAmount& operator=(Number const&); @@ -212,17 +470,32 @@ public: // //-------------------------------------------------------------------------- + /** Flip the sign; a canonical zero amount is left unchanged. */ void negate(); + /** Reset to zero while keeping the current asset identity. + * + * For IOU amounts sets `offset` to −100 (the canonical zero sentinel so + * that zero sorts below small positive IOUs). For integral types sets + * `offset` to 0. + */ void clear(); - // Zero while copying currency and issuer. + /** Reset to zero with a new asset identity. + * + * Equivalent to `setIssue(asset); clear();`. + * + * @param asset The asset to adopt. + */ void clear(Asset const& asset); - /** Set the Issue for this amount. */ + /** Replace the asset identity without changing the value representation. + * + * @param asset New asset (Issue or MPTIssue). + */ void setIssue(Asset const& asset); @@ -232,30 +505,68 @@ public: // //-------------------------------------------------------------------------- + /** Returns `STI_AMOUNT`. */ [[nodiscard]] SerializedTypeID getSType() const override; + /** Returns a human-readable string including the field name and formatted value. */ [[nodiscard]] std::string getFullText() const override; + /** Returns a formatted string representation of the numeric value. */ [[nodiscard]] std::string getText() const override; + /** Serialize to JSON. + * + * XRP amounts are emitted as a plain decimal string (drop count). + * IOU amounts produce `{value, currency, issuer}`. + * MPT amounts produce `{value, mpt_issuance_id}`. + */ [[nodiscard]] json::Value getJson(JsonOptions = JsonOptions::Values::None) const override; + /** Append the wire-format encoding to `s`. + * + * Writes the compact 64-bit word plus any trailing currency/issuer + * bytes (IOU) or 192-bit MPTID (MPT). + */ void add(Serializer& s) const override; + /** Returns true if `t` is an `STAmount` with the same asset and value. + * + * Comparison is performed on the binary representation, so canonical + * equivalence is checked, not numeric equality. + */ [[nodiscard]] bool isEquivalent(STBase const& t) const override; + /** Returns true when the amount is zero. + * + * A field whose presence is governed by `soeDEFAULT` is omitted from + * ledger serialisation when `isDefault()` is true. + */ [[nodiscard]] bool isDefault() const override; + /** Extract the value as a lean `XRPAmount`. + * + * @throws std::logic_error if this is not a native XRP amount. + */ [[nodiscard]] XRPAmount xrp() const; + + /** Extract the value as a lean `IOUAmount`. + * + * @throws std::logic_error if this is not an IOU amount. + */ [[nodiscard]] IOUAmount iou() const; + + /** Extract the value as a lean `MPTAmount`. + * + * @throws std::logic_error if this is not an MPT amount. + */ [[nodiscard]] MPTAmount mpt() const; @@ -354,7 +665,6 @@ STAmount::STAmount(A const& asset, int mantissa, int exponent) { } -// Legacy support for new-style amounts inline STAmount::STAmount(IOUAmount const& amount, Issue const& issue) : asset_(issue), offset_(amount.exponent()), isNegative_(amount < beast::kZERO) { @@ -391,21 +701,70 @@ inline STAmount::STAmount(MPTAmount const& amount, MPTIssue const& mptIssue) // //------------------------------------------------------------------------------ -// VFALCO TODO The parameter type should be Quality not uint64_t +/** Reconstruct an offer quality (rate) as a displayable STAmount. + * + * Decodes the packed `uint64_t` quality word produced by `getRate()` back + * into a human-readable IOU-denominated amount (no issuer). + * + * @param rate Encoded quality word (exponent in high byte, mantissa in low bits). + * @return An STAmount suitable for display or JSON output. + * @note The parameter type should eventually be `Quality` rather than `uint64_t`. + */ STAmount amountFromQuality(std::uint64_t rate); +/** Parse an amount from a decimal string for the given asset. + * + * Accepts a plain decimal string (possibly with an exponent suffix for IOU) + * or a drop-count string for XRP. Throws on malformed input. + * + * @param asset Target asset type. + * @param amount Decimal string representation. + * @return The parsed STAmount. + * @throws std::runtime_error on malformed input. + */ STAmount amountFromString(Asset const& asset, std::string const& amount); +/** Parse an STAmount from a JSON value, associating it with a named SField. + * + * Accepts three formats: + * - Plain string (XRP drop count). + * - `{value, currency, issuer}` object (IOU). + * - `{value, mpt_issuance_id}` object (MPT). + * + * Also accepts the legacy slash-delimited string format used in some RPC + * responses for historical compatibility. + * + * @param name SField to associate with the resulting STAmount. + * @param v JSON value to parse. + * @return The parsed STAmount. + * @throws std::runtime_error if the JSON is malformed or the values are out of range. + */ STAmount amountFromJson(SField const& name, json::Value const& v); +/** Non-throwing variant of `amountFromJson`. + * + * Parses a JSON value as an STAmount. On success writes to `result` and + * returns true; on any error leaves `result` unchanged and returns false. + * + * @param result Output parameter filled on success. + * @param jvSource JSON value to parse. + * @return True on success, false on any parse error. + */ bool amountFromJsonNoThrow(STAmount& result, json::Value const& jvSource); -// IOUAmount and XRPAmount define toSTAmount, defining this -// trivial conversion here makes writing generic code easier +/** Identity conversion so generic code can call `toSTAmount()` uniformly. + * + * `IOUAmount` and `XRPAmount` provide their own `toSTAmount()` overloads. + * This overload completes the set so that templates need not special-case + * `STAmount`. + * + * @param a The STAmount to pass through. + * @return A const reference to `a`. + */ inline STAmount const& toSTAmount(STAmount const& a) { @@ -555,8 +914,6 @@ STAmount::negate() inline void STAmount::clear() { - // The -100 is used to allow 0 to sort less than a small positive values - // which have a negative exponent. offset_ = integral() ? 0 : -100; value_ = 0; isNegative_ = false; @@ -575,6 +932,14 @@ STAmount::value() const noexcept return *this; } +/** Returns true if the amount is a legal network value. + * + * For non-native amounts this is always true. For XRP amounts, the mantissa + * must not exceed `STAmount::kMAX_NATIVE_N` (10^17 drops = 100 billion XRP). + * Amounts that fail this check must not be included in consensus transactions. + * + * @param value The amount to test. + */ inline bool isLegalNet(STAmount const& value) { @@ -587,35 +952,55 @@ isLegalNet(STAmount const& value) // //------------------------------------------------------------------------------ +/** Compare two STAmounts for equality. + * + * Two amounts are equal when they have identical asset, mantissa, exponent, + * and sign. Amounts of different asset types are never equal. + */ bool operator==(STAmount const& lhs, STAmount const& rhs); + +/** Less-than comparison for STAmount. + * + * Defines a total order within the same asset type. Amounts of different + * asset types compare by asset identity first (implementation-defined stable + * order) so that STAmount can be used in ordered containers. + */ bool operator<(STAmount const& lhs, STAmount const& rhs); +/** Returns `!(lhs == rhs)`. */ inline bool operator!=(STAmount const& lhs, STAmount const& rhs) { return !(lhs == rhs); } +/** Returns `rhs < lhs`. */ inline bool operator>(STAmount const& lhs, STAmount const& rhs) { return rhs < lhs; } +/** Returns `!(rhs < lhs)`. */ inline bool operator<=(STAmount const& lhs, STAmount const& rhs) { return !(rhs < lhs); } +/** Returns `!(lhs < rhs)`. */ inline bool operator>=(STAmount const& lhs, STAmount const& rhs) { return !(lhs < rhs); } +/** Return the arithmetic negation of `value`. + * + * A zero amount is returned unchanged (canonical zero has no sign). + */ STAmount operator-(STAmount const& value); @@ -625,36 +1010,110 @@ operator-(STAmount const& value); // //------------------------------------------------------------------------------ +/** Add two same-asset STAmounts. + * + * @pre `v1` and `v2` must have the same asset. + */ STAmount operator+(STAmount const& v1, STAmount const& v2); + +/** Subtract two same-asset STAmounts. + * + * @pre `v1` and `v2` must have the same asset. + */ STAmount operator-(STAmount const& v1, STAmount const& v2); +/** Divide `v1` by `v2`, expressing the result in `asset`. + * + * Designed for cross-currency calculations where the result naturally belongs + * to a third asset (e.g. quality calculations). Uses the amendment-gated + * arithmetic path (`getSTNumberSwitchover()`) for precision. + * + * @param v1 Dividend. + * @param v2 Divisor (must be non-zero). + * @param asset Asset type for the result. + * @return Quotient expressed as an STAmount with `asset`. + */ STAmount divide(STAmount const& v1, STAmount const& v2, Asset const& asset); +/** Multiply `v1` by `v2`, expressing the result in `asset`. + * + * @param v1 First factor. + * @param v2 Second factor. + * @param asset Asset type for the result. + * @return Product expressed as an STAmount with `asset`. + */ STAmount multiply(STAmount const& v1, STAmount const& v2, Asset const& asset); -// multiply rounding result in specified direction +/** Multiply with legacy fixed-direction rounding. + * + * Uses the legacy rounding approach: rounds up when the fractional + * remainder is ≥ 0.1 of the smallest representable unit. + * Prefer `mulRoundStrict` for new code that needs accurate rounding. + * + * @param v1 First factor. + * @param v2 Second factor. + * @param asset Asset type for the result. + * @param roundUp True to round up, false to round down. + * @return Rounded product expressed as an STAmount with `asset`. + */ STAmount mulRound(STAmount const& v1, STAmount const& v2, Asset const& asset, bool roundUp); -// multiply following the rounding directions more precisely. +/** Multiply following the thread-local `Number::rounding_mode` precisely. + * + * Respects the `NumberRoundModeGuard` rounding mode for accurate remainder + * tracking, rather than the fixed legacy approximation used by `mulRound`. + * + * @param v1 First factor. + * @param v2 Second factor. + * @param asset Asset type for the result. + * @param roundUp True to round up, false to round down. + * @return Rounded product expressed as an STAmount with `asset`. + */ STAmount mulRoundStrict(STAmount const& v1, STAmount const& v2, Asset const& asset, bool roundUp); -// divide rounding result in specified direction +/** Divide with legacy fixed-direction rounding. + * + * Uses the legacy rounding approach. Prefer `divRoundStrict` for new code. + * + * @param v1 Dividend. + * @param v2 Divisor (must be non-zero). + * @param asset Asset type for the result. + * @param roundUp True to round up, false to round down. + * @return Rounded quotient expressed as an STAmount with `asset`. + */ STAmount divRound(STAmount const& v1, STAmount const& v2, Asset const& asset, bool roundUp); -// divide following the rounding directions more precisely. +/** Divide following the thread-local `Number::rounding_mode` precisely. + * + * @param v1 Dividend. + * @param v2 Divisor (must be non-zero). + * @param asset Asset type for the result. + * @param roundUp True to round up, false to round down. + * @return Rounded quotient expressed as an STAmount with `asset`. + */ STAmount divRoundStrict(STAmount const& v1, STAmount const& v2, Asset const& asset, bool roundUp); -// 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 +/** Encode an offer quality (in/out ratio) as a compact `uint64_t`. + * + * The rate represents `offerIn / offerOut`. A **smaller** value is better + * for the taker (more output per unit input). The encoding packs the + * base-10 exponent in the high byte and the mantissa in the remaining bits, + * making the values directly comparable as integers — which is the sort + * order used for offer-book directories. + * + * @param offerOut Amount the offer gives out. + * @param offerIn Amount the offer takes in. + * @return Packed quality word, or 0 if the result underflows. + * @note The return type should eventually be `Quality`. + */ std::uint64_t getRate(STAmount const& offerOut, STAmount const& offerIn); @@ -722,26 +1181,63 @@ roundToAsset( //------------------------------------------------------------------------------ +/** Returns true if `amount` represents native XRP. + * + * Convenience wrapper around `STAmount::native()` for use in generic code + * that checks the asset type before dispatching. + */ inline bool isXRP(STAmount const& amount) { return amount.native(); } +/** Pre-flight check: returns true if `amt1 + amt2` is representable. + * + * For XRP and MPT amounts this performs 64-bit overflow/underflow bounds + * tests without executing the addition. + * + * For IOU amounts a relative-precision metric is used: both operands are + * reconstructed after a round-trip through addition and the combined + * relative error must not exceed 10^-4. This guards against silently + * losing significant digits when the operands' exponents differ by more + * than 15 (the mantissa precision limit). + * + * @param amt1 First operand. + * @param amt2 Second operand. + * @return True if the addition can be performed safely; false if it would + * overflow or produce an unacceptably imprecise result. + */ bool canAdd(STAmount const& amt1, STAmount const& amt2); +/** Pre-flight check: returns true if `amt1 - amt2` is representable. + * + * Equivalent to `canAdd(amt1, -amt2)`. Performs 64-bit underflow/overflow + * bounds tests for XRP and MPT; uses the relative-precision metric for IOU. + * + * @param amt1 Minuend. + * @param amt2 Subtrahend. + * @return True if the subtraction can be performed safely. + */ bool canSubtract(STAmount const& amt1, STAmount const& amt2); -/** Get the scale of a Number for a given asset. +/** Return the STAmount exponent that would result from converting `number` + * to an STAmount for the given asset. * - * "scale" is similar to "exponent", but from the perspective of STAmount, which has different rules - * and mantissa ranges for determining the exponent than Number. + * "Scale" is the base-10 exponent after STAmount normalization, which + * differs from `Number::exponent()` because STAmount enforces a narrower + * mantissa range (`[kMIN_VALUE, kMAX_VALUE]`) and asset-specific rules + * (integral assets always have exponent 0). This function constructs a + * temporary STAmount purely to read back the normalized exponent. * - * @param number The Number to get the scale of. - * @param asset The asset to use for determining the scale. - * @return The scale of this Number for the given asset. + * Used by `roundToAsset` to determine the precision boundary before + * shedding sub-precision dust via `roundToScale`. + * + * @param number The high-precision value to inspect. + * @param asset The asset that governs normalization rules. + * @return The base-10 exponent of the normalized STAmount. */ inline int scale(Number const& number, Asset const& asset) @@ -753,6 +1249,20 @@ scale(Number const& number, Asset const& asset) //------------------------------------------------------------------------------ namespace json { + +/** Extract an STAmount from a JSON object by SField name. + * + * Specialisation of `json::getOrThrow` for `xrpl::STAmount`. Looks up + * the field by its JSON key name in `v`, then delegates to + * `xrpl::amountFromJson` for full parsing (handles XRP string, IOU object, + * and MPT object formats). + * + * @param v JSON object containing the field. + * @param field SField whose JSON name is used as the lookup key. + * @return Parsed STAmount. + * @throws JsonMissingKeyError if the key is absent in `v`. + * @throws std::runtime_error if the value cannot be parsed as an STAmount. + */ template <> inline xrpl::STAmount getOrThrow(json::Value const& v, xrpl::SField const& field) diff --git a/include/xrpl/protocol/STArray.h b/include/xrpl/protocol/STArray.h index 61753c52dc..8b25046d71 100644 --- a/include/xrpl/protocol/STArray.h +++ b/include/xrpl/protocol/STArray.h @@ -5,6 +5,28 @@ namespace xrpl { +/** An ordered, variable-length sequence of `STObject` instances. + * + * `STArray` is the protocol's container for repeated structured sub-fields + * within transactions and ledger entries — for example `sfMemos` (per-tx + * memo objects), `sfSigners` (multi-sign signer list), and `sfNFTokens` (NFT + * page entries). It participates fully in the XRPL binary wire format and + * the JSON/RPC layer. + * + * The binary format is sentinel-terminated: elements are encoded sequentially + * and the stream ends with an `(STI_ARRAY, 1)` marker rather than a length + * prefix. Each element must be an `STObject`; non-object field types and + * misplaced terminators throw `std::runtime_error` during deserialization. + * + * Instances are tracked by `CountedObject` for diagnostic purposes + * (see `GetCounts`). The tracking cost is a single atomic + * increment/decrement per object lifetime. + * + * @note An empty `STArray` is considered the default value (`isDefault()` + * returns `true`), so enclosing `STObject` serializers will omit it from + * the wire encoding entirely — consistent with how absent optional array + * fields are represented in the ledger. + */ class STArray final : public STBase, public CountedObject { private: @@ -21,12 +43,30 @@ public: STArray() = default; STArray(STArray const&) = default; + /** Construct an anonymous STArray from an iterator range of `STObject`s. + * + * The resulting array has no `SField` association. Use the two-argument + * overload when the array must be bound to a named field. + * + * @tparam Iter Forward iterator whose reference type is convertible to + * `STObject`. + * @param first Beginning of the source range. + * @param last One-past-the-end of the source range. + */ template < class Iter, class = std::enable_if_t< std::is_convertible_v::reference, STObject>>> explicit STArray(Iter first, Iter last); + /** Construct an STArray bound to a field, initialized from an iterator range. + * + * @tparam Iter Forward iterator whose reference type is convertible to + * `STObject`. + * @param f The `SField` that names this array in its parent object. + * @param first Beginning of the source range. + * @param last One-past-the-end of the source range. + */ template < class Iter, class = std::enable_if_t< @@ -35,34 +75,131 @@ public: STArray& operator=(STArray const&) = default; + + /** Move constructor. + * + * Explicitly copies the `SField` name from @p other before moving the + * element vector. `STBase` stores the field-name pointer separately from + * the data, so without this explicit transfer the moved-into object would + * carry a stale field association, causing field-ID mismatches during + * serialization. + * + * @param other The array to move from; left in a valid but unspecified state. + */ STArray(STArray&&); + + /** Move assignment operator. + * + * Same field-name transfer requirement as the move constructor. + * + * @param other The array to move from; left in a valid but unspecified state. + * @return `*this` + */ STArray& operator=(STArray&&); + /** Construct an STArray bound to a field with pre-allocated capacity. + * + * @param f The `SField` that names this array in its parent object. + * @param n Number of elements to reserve storage for. + */ STArray(SField const& f, std::size_t n); + + /** Deserializing constructor — decodes a sentinel-terminated sequence of + * inner objects from a binary stream. + * + * Loops over `(type, field)` pairs from @p sit until the canonical + * end-of-array marker (`STI_ARRAY, field == 1`) is encountered. Each + * iteration validates the next token and constructs an `STObject` element + * in place. After construction, `applyTemplateFromSField` validates the + * element against the registered schema for its field type (e.g. `sfMemo`, + * `sfSigner`). + * + * @param sit Forward cursor over the binary payload. Advanced in place. + * @param f The `SField` naming this array in its parent object. + * @param depth Current nesting depth threaded from the parent `STObject`. + * Incremented before each child `STObject` is constructed; `STObject` + * enforces a maximum depth of 10 to prevent stack exhaustion from + * crafted payloads. + * @throws std::runtime_error with message `"Illegal terminator in array"` + * if a misplaced end-of-object marker `(STI_OBJECT, 1)` is found. + * @throws std::runtime_error with message `"Unknown field"` if an + * unrecognized `(type, field)` pair is encountered. + * @throws std::runtime_error with message `"Non-object in array"` if a + * non-`STI_OBJECT` element type appears in the stream. + * @throws std::runtime_error if `applyTemplateFromSField` rejects an + * element; the partially constructed array is abandoned entirely. + */ STArray(SerialIter& sit, SField const& f, int depth = 0); + + /** Construct an anonymous STArray with pre-allocated capacity. + * + * Creates an array with no `SField` association but with storage reserved + * for @p n elements, avoiding early reallocations when the size is known + * up front. + * + * @param n Number of elements to reserve space for. + */ explicit STArray(int n); + + /** Construct an empty STArray bound to the given field. + * + * @param f The `SField` that names this array in its parent object. + */ explicit STArray(SField const& f); + /** Access element at index @p j without bounds checking. + * + * @param j Zero-based index; behaviour is undefined if `j >= size()`. + * @return Reference to the element at position @p j. + */ STObject& operator[](std::size_t j); + /** Access element at index @p j without bounds checking (const overload). + * + * @param j Zero-based index; behaviour is undefined if `j >= size()`. + * @return Const reference to the element at position @p j. + */ STObject const& operator[](std::size_t j) const; + /** Access the last element without bounds checking. + * + * @return Reference to the last element; behaviour is undefined if the + * array is empty. + */ STObject& back(); + /** Access the last element without bounds checking (const overload). + * + * @return Const reference to the last element; behaviour is undefined if + * the array is empty. + */ [[nodiscard]] STObject const& back() const; + /** Construct an `STObject` in place at the end of the array. + * + * @tparam Args Constructor argument types forwarded to `STObject`. + * @param args Arguments forwarded to the `STObject` constructor. + */ template void emplaceBack(Args&&... args); + /** Append a copy of @p object to the end of the array. + * + * @param object The element to copy-append. + */ void pushBack(STObject const& object); + /** Append @p object to the end of the array by move. + * + * @param object The element to move-append. + */ void pushBack(STObject&& object); @@ -81,72 +218,187 @@ public: pushBack(std::move(object)); } + /** Return an iterator to the first element. */ iterator begin(); + /** Return an iterator to one past the last element. */ iterator end(); + /** Return a const iterator to the first element. */ [[nodiscard]] const_iterator begin() const; + /** Return a const iterator to one past the last element. */ [[nodiscard]] const_iterator end() const; + /** Return the number of elements in the array. */ [[nodiscard]] size_type size() const; + /** Return `true` when the array contains no elements. */ [[nodiscard]] bool empty() const; + /** Remove all elements, leaving an empty array. */ void clear(); + /** Reserve storage for at least @p n elements without changing the size. + * + * @param n Minimum capacity to reserve. + */ void reserve(std::size_t n); + /** Swap contents with @p a in constant time. + * + * @param a The other array to swap with. + */ void swap(STArray& a) noexcept; + /** Return a bracket-delimited, comma-separated string including field names. + * + * Each element is rendered via `STObject::getFullText()`. Intended for + * debugging and logging. + * + * @return Human-readable representation such as `[fieldA = ..., fieldB = ...]`. + */ [[nodiscard]] std::string getFullText() const override; + /** Return a bracket-delimited, comma-separated string of element values only. + * + * Each element is rendered via `STObject::getText()`, which omits field-name + * prefixes. Intended for debugging. + * + * @return Human-readable value-only representation of the array. + */ [[nodiscard]] std::string getText() const override; + /** Serialize this array to a JSON array value. + * + * Each element that is not `STI_NOTPRESENT` is appended as a JSON object + * with a single key — the element's field name — mapping to the element's + * own JSON representation: + * @code + * [ { "Memo": { "MemoData": "..." } }, ... ] + * @endcode + * Elements with type `STI_NOTPRESENT` (absent optional fields in a + * template-bound context) are silently skipped. + * + * @param index JSON rendering options forwarded to each element. + * @return A `json::Value` of array type. + */ [[nodiscard]] json::Value getJson(JsonOptions index) const override; + /** Append the binary encoding of every element to @p s. + * + * For each element, writes: field ID, element content, per-element object + * terminator (`STI_OBJECT, 1`). The outer array's own field ID and the + * end-of-array terminator (`STI_ARRAY, 1`) are written by the enclosing + * `STObject`, not here — each level is responsible only for its own body. + * + * @param s The serializer to append to. + */ void add(Serializer& s) const override; + /** Sort elements in place using a caller-supplied strict-weak-order comparator. + * + * Used to impose canonical ordering before serialization. Key callers: + * - `TxMeta::addRaw()` — sorts `AffectedNodes` by `sfLedgerIndex`; a + * deviation is a consensus-fork risk. + * - NFToken helpers — sorts `sfNFTokens` entries by `sfNFTokenID` when + * managing NFT pages. + * + * @note Multi-sign transactions require `sfSigners` to be sorted in + * ascending `AccountID` order before submission. That sort is expected + * to be performed by the signing client, not by the protocol layer. + * + * @param compare Function pointer returning `true` when the first argument + * should precede the second. Must satisfy strict-weak-ordering. + */ void sort(bool (*compare)(STObject const& o1, STObject const& o2)); + /** Test element-wise equality with another `STArray`. + * + * @param s The array to compare against. + * @return `true` if both arrays have the same number of elements and each + * pair of corresponding elements compares equal via `STObject::operator==`. + */ bool operator==(STArray const& s) const; + /** Test element-wise inequality with another `STArray`. + * + * @param s The array to compare against. + * @return `true` if the arrays differ in size or any element pair is unequal. + */ bool operator!=(STArray const& s) const; + /** Erase the element at @p pos. + * + * @param pos Iterator to the element to remove. + * @return Iterator to the element following the erased one. + */ iterator erase(iterator pos); + /** Erase the element at @p pos (const_iterator overload). + * + * @param pos Const iterator to the element to remove. + * @return Iterator to the element following the erased one. + */ iterator erase(const_iterator pos); + /** Erase the elements in the range `[first, last)`. + * + * @param first Iterator to the first element to remove. + * @param last Iterator to one past the last element to remove. + * @return Iterator to the element following the last erased element. + */ iterator erase(iterator first, iterator last); + /** Erase the elements in the range `[first, last)` (const_iterator overload). + * + * @param first Const iterator to the first element to remove. + * @param last Const iterator to one past the last element to remove. + * @return Iterator to the element following the last erased element. + */ iterator erase(const_iterator first, const_iterator last); + /** Return `STI_ARRAY` — the serialized type ID for this class. */ [[nodiscard]] SerializedTypeID getSType() const override; + /** Test deep equality with another `STBase`. + * + * Performs a `dynamic_cast` to confirm @p t is also an `STArray`, then + * delegates to vector equality, which cascades through `STObject::operator==`. + * + * @param t The object to compare against. + * @return `true` if @p t is an `STArray` whose elements are pairwise equal + * to this array's elements. + */ [[nodiscard]] bool isEquivalent(STBase const& t) const override; + /** Return `true` when the array is empty. + * + * An empty `STArray` is the default value; the enclosing `STObject` + * serializer will omit a default-valued field from the wire encoding. + */ [[nodiscard]] bool isDefault() const override; diff --git a/include/xrpl/protocol/STBase.h b/include/xrpl/protocol/STBase.h index bfcc50d1ff..4661bf58ea 100644 --- a/include/xrpl/protocol/STBase.h +++ b/include/xrpl/protocol/STBase.h @@ -12,34 +12,48 @@ namespace xrpl { -/// Note, should be treated as flags that can be | and & +/** Bitmask controlling how an ST type renders to JSON. + * + * Combines named flag bits defined in `Values`. Supports `|`, `&`, and `~` + * for composing and masking option sets. The complement operator `~` is + * bounded by `Values::All` so it never sets reserved future bits. + * + * @note Treat instances as flag sets — bitwise operators are the intended + * interface; do not compare or store the raw `value` field directly. + */ struct JsonOptions { using underlying_t = unsigned int; underlying_t value; + /** Named option bits for JSON rendering. */ enum class Values : underlying_t { None = 0b0000'0000, - IncludeDate = 0b0000'0001, - DisableApiPriorV2 = 0b0000'0010, + IncludeDate = 0b0000'0001, /**< Include a date field in the output. */ + DisableApiPriorV2 = 0b0000'0010, /**< Suppress legacy pre-API-v2 formatting. */ // IMPORTANT `All` must be union of all of the above; see also operator~ All = IncludeDate | DisableApiPriorV2 // 0b0000'0011 }; + /** Construct from a raw bitmask value. */ constexpr JsonOptions(underlying_t v) noexcept : value(v) { } + /** Construct from a named `Values` enumerator. */ constexpr JsonOptions(Values v) noexcept : value(static_cast(v)) { } + /** Convert to the underlying unsigned integer. */ [[nodiscard]] constexpr explicit operator underlying_t() const noexcept { return value; } + + /** Return `true` if any option bit is set. */ [[nodiscard]] constexpr explicit operator bool() const noexcept { @@ -50,22 +64,26 @@ struct JsonOptions [[nodiscard]] constexpr auto friend operator!=(JsonOptions lh, JsonOptions rh) noexcept -> bool = default; - /// Returns JsonOptions union of lh and rh + /** Return the union (bitwise OR) of two option sets. */ [[nodiscard]] constexpr JsonOptions friend operator|(JsonOptions lh, JsonOptions rh) noexcept { return {lh.value | rh.value}; } - /// Returns JsonOptions intersection of lh and rh + /** Return the intersection (bitwise AND) of two option sets. */ [[nodiscard]] constexpr JsonOptions friend operator&(JsonOptions lh, JsonOptions rh) noexcept { return {lh.value & rh.value}; } - /// Returns JsonOptions binary negation, can be used with & (above) for set - /// difference e.g. `(options & ~JsonOptions::kINCLUDE_DATE)` + /** Return the complement, bounded to the known `Values::All` mask. + * + * Use with `&` for set-difference, e.g. + * `options & ~JsonOptions(JsonOptions::Values::IncludeDate)`. + * Bits beyond `Values::All` are never set in the result. + */ [[nodiscard]] constexpr JsonOptions friend operator~(JsonOptions v) noexcept { @@ -73,6 +91,17 @@ struct JsonOptions } }; +/** ADL-accessible JSON conversion for any type that exposes `getJson(JsonOptions)`. + * + * Calls `t.getJson(JsonOptions::Values::None)`. Provides a uniform, + * options-free entry point for generic code that needs to render an ST value + * without caring about per-call rendering flags. + * + * @tparam T A type whose `getJson` method returns a value convertible to + * `json::Value`. + * @param t The object to convert. + * @return A `json::Value` representation of @p t. + */ template requires requires(T const& t) { { t.getJson(JsonOptions::Values::None) } -> std::convertible_to; @@ -100,85 +129,214 @@ class STVar; //------------------------------------------------------------------------------ -/** A type which can be exported to a well known binary format. - - A STBase: - - Always a field - - Can always go inside an eligible enclosing STBase - (such as STArray) - - Has a field name - - Like JSON, a SerializedObject is a basket which has rules - on what it can hold. - - @note "ST" stands for "Serialized Type." -*/ +/** Abstract base class for every serialized field type in the XRPL protocol. + * + * "ST" stands for "Serialized Type." Every field that appears in a + * transaction, ledger entry, or validation — integers, amounts, account IDs, + * blobs, arrays, nested objects — is represented as a class derived from + * `STBase`. Each instance carries a field identity (an `SField` pointer) that + * binds a human-readable name and a numeric type+field code used in the binary + * wire format. + * + * The virtual interface (`getSType`, `add`, `isEquivalent`, `isDefault`, + * `getJson`, `getText`, `getFullText`) must be overridden by every concrete + * subclass. + * + * @note `operator=` deliberately does **not** copy the field name when the + * destination already holds a meaningful name (see implementation). This + * design supports element slide-down in `STObject`/`STArray` without + * corrupting field identities. As a consequence, do **not** store + * `STBase`-derived objects directly in `std::vector` or similar owning + * containers — use `boost::ptr_*` containers or `detail::STVar` instead. + */ class STBase { SField const* fName_; public: virtual ~STBase() = default; + + /** Construct with the generic (placeholder) field name. */ STBase(); + STBase(STBase const&) = default; + + /** Copy-assign the value; conditionally copies the field name. + * + * The field name (`fName_`) is updated from @p t only when the current + * name is not useful (e.g., `sfGeneric`). This allows slot initialisation + * to pick up the source's protocol identity while preventing element + * slide-down operations inside `STObject` from overwriting already-valid + * field names. + * + * @param t The source object whose value (and optionally name) to copy. + * @return `*this` + */ STBase& operator=(STBase const& t); + /** Construct with a specific field identity. + * + * @param n The `SField` descriptor that identifies this field on the wire. + */ explicit STBase(SField const& n); + /** Value equality: same concrete type and `isEquivalent` holds. + * + * Field names are ignored; only values are compared. + */ bool operator==(STBase const& t) const; + + /** Value inequality: opposite of `operator==`. */ bool operator!=(STBase const& t) const; + /** Narrow the static type to `D`, throwing on failure. + * + * Performs `dynamic_cast(this)`. Prefer this over a raw + * `dynamic_cast` at call sites — it guarantees that a failed cast throws + * `std::bad_cast` rather than yielding a null pointer that may be + * silently dereferenced. + * + * @tparam D The target derived type. + * @return A reference to `*this` as `D`. + * @throws std::bad_cast if `*this` is not an instance of `D`. + */ template D& downcast(); + /** Const overload of `downcast()`. + * + * @tparam D The target derived type. + * @return A const reference to `*this` as `D`. + * @throws std::bad_cast if `*this` is not an instance of `D`. + */ template D const& downcast() const; + /** Return the `SerializedTypeID` enum value for this concrete type. + * + * The base implementation returns `STI_NOTPRESENT`. Every concrete + * subclass overrides this to return its own type tag. + */ [[nodiscard]] virtual SerializedTypeID getSType() const; + /** Return a human-readable string that includes the field name. + * + * Typically formatted as `" = "`. Returns an empty + * string when `getSType() == STI_NOTPRESENT`. + */ [[nodiscard]] virtual std::string getFullText() const; + /** Return a human-readable string representation of the value only. + * + * Unlike `getFullText()`, the field name is not included. The base + * implementation returns an empty string. + */ [[nodiscard]] virtual std::string getText() const; + /** Render to a JSON value, respecting the given rendering options. + * + * @param options Bitmask controlling date inclusion and API version + * formatting. Defaults to `JsonOptions::Values::None`. + * @return A `json::Value` representation. + */ [[nodiscard]] virtual json::Value getJson(JsonOptions = JsonOptions::Values::None) const; + /** Serialize the field's binary payload into @p s. + * + * Writes only the value bytes; the field-ID header must be written + * separately via `addFieldID()`. The base implementation is an + * unreachable stub — every concrete subclass must override this. + * + * @param s The `Serializer` accumulator to write into. + */ virtual void add(Serializer& s) const; + /** Value equivalence check, ignoring field names. + * + * Used by `operator==` and by `detail::STVar::operator==`. The base + * implementation asserts that this instance has type `STI_NOTPRESENT` + * and returns `true` only if @p t does as well. All concrete subclasses + * must override this. + * + * @param t The object to compare against. + * @return `true` if the two objects hold equivalent values. + */ [[nodiscard]] virtual bool isEquivalent(STBase const& t) const; + /** Return `true` if the field holds its default (zero-equivalent) value. + * + * Used during serialization to omit optional fields whose value is + * the type default. The base implementation always returns `true`. + */ [[nodiscard]] virtual bool isDefault() const; - /** A STBase is a field. - This sets the name. - */ + /** Set the field identity for this instance. + * + * @param n The `SField` descriptor to associate with this object. + */ void setFName(SField const& n); + /** Return the `SField` descriptor that identifies this field. */ [[nodiscard]] SField const& getFName() const; + /** Write the type+field ID prefix bytes to @p s. + * + * Encodes the combined type code and field code as 1–3 bytes per the + * XRPL binary format, forming the header that precedes the value bytes + * written by `add()`. Asserts that the current field is a binary + * (wire-representable) field. + * + * @param s The `Serializer` to write the field ID into. + */ void addFieldID(Serializer& s) const; protected: + /** Placement helper for the small-object optimization used by `detail::STVar`. + * + * If `sizeof(U) <= n`, constructs a `U` in @p buf via placement-new and + * returns a pointer to it. Otherwise heap-allocates a `U` with `new`. + * Concrete subclasses delegate to this from their `copy()` and `move()` + * overrides so that `detail::STVar` can store small types inline without + * a separate heap allocation. + * + * @tparam T The value type to construct (deduced); `U = std::decay_t`. + * @param n Size of the inline buffer in bytes. + * @param buf Pointer to inline storage of at least @p n bytes. + * @param val The value to forward-construct into the buffer or heap. + * @return Pointer to the newly constructed `U` object. + */ template static STBase* emplace(std::size_t n, void* buf, T&& val); private: + /** Copy this object into @p buf (or heap) and return a pointer to it. + * + * Called exclusively by `detail::STVar` to implement copy construction. + * Delegates to `emplace(n, buf, *this)`. + */ virtual STBase* copy(std::size_t n, void* buf) const; + + /** Move this object into @p buf (or heap) and return a pointer to it. + * + * Called exclusively by `detail::STVar` to implement move construction. + * Delegates to `emplace(n, buf, std::move(*this))`. + */ virtual STBase* move(std::size_t n, void* buf); @@ -187,6 +345,14 @@ private: //------------------------------------------------------------------------------ +/** Stream an `STBase` as its full-text representation (field name and value). + * + * Equivalent to `out << t.getFullText()`. + * + * @param out The output stream to write to. + * @param t The serialized-type object to render. + * @return @p out, to allow chaining. + */ std::ostream& operator<<(std::ostream& out, STBase const& t); diff --git a/include/xrpl/protocol/STBitString.h b/include/xrpl/protocol/STBitString.h index 8a7e5a6030..4eb709be6c 100644 --- a/include/xrpl/protocol/STBitString.h +++ b/include/xrpl/protocol/STBitString.h @@ -1,3 +1,10 @@ +/** @file + * Defines `STBitString`, the serialization-layer representation of + * fixed-width opaque bit arrays, and the four concrete aliases used + * throughout the protocol: `STUInt128`, `STUInt160`, `STUInt192`, and + * `STUInt256`. + */ + #pragma once #include @@ -6,16 +13,34 @@ namespace xrpl { -// The template parameter could be an unsigned type, however there's a bug in -// gdb (last checked in gdb 12.1) that prevents gdb from finding the RTTI -// information of a template parameterized by an unsigned type. This RTTI -// information is needed to write gdb pretty printers. +/** Serialized fixed-width bit string field in the XRPL protocol type system. + * + * Bridges `BaseUInt` — used for transaction hashes, account IDs, + * ledger indices, and similar opaque identifiers — and the `STBase` + * serialization framework. Despite the underlying type supporting + * arithmetic, this class treats its value as an opaque sequence of bits: + * only identity comparison, serialization, and value access are exposed. + * + * Each concrete instantiation (128, 160, 192, 256 bits) returns a + * distinct wire-type code from `getSType()` (`STI_UINT128` through + * `STI_UINT256`), so field metadata and codec behavior are type-correct at + * the protocol level. + * + * `CountedObject>` maintains a per-width live instance + * counter for diagnostic reporting; it carries no functional overhead. + * + * @tparam Bits Number of bits in the value. Declared `int` rather than + * `unsigned int` to work around a GDB 12.1 bug that prevents locating + * RTTI for templates instantiated over unsigned types; a `static_assert` + * enforces that the value is positive. + */ template class STBitString final : public STBase, public CountedObject> { static_assert(Bits > 0, "Number of bits must be positive"); public: + /** The underlying tag-free bit-string type (`BaseUInt`). */ using value_type = BaseUInt; private: @@ -24,47 +49,159 @@ private: public: STBitString() = default; + /** Construct a named field with a zero-initialized value. + * + * Used when building objects programmatically before the value is known. + * + * @param n The `SField` that identifies this field on the wire. + */ STBitString(SField const& n); + + /** Construct an anonymous value, discarding field identity. + * + * Intended for temporary computations where only the raw value matters. + * + * @param v The initial bit-string value. + */ STBitString(value_type const& v); + + /** Construct a fully specified named field with a given value. + * + * @param n The `SField` that identifies this field on the wire. + * @param v The initial bit-string value. + */ STBitString(SField const& n, value_type const& v); + + /** Deserialize a named field from a byte stream. + * + * Reads exactly `Bits/8` bytes from `sit` at the current cursor + * position via `SerialIter::getBitString()`, centralizing + * deserialization logic in `SerialIter`. + * + * @param sit The input cursor; advanced by `Bits/8` bytes on success. + * @param name The `SField` that identifies this field on the wire. + * @throws std::runtime_error if the stream has fewer than `Bits/8` + * bytes remaining. + */ STBitString(SerialIter& sit, SField const& name); + /** Return the wire-type identifier for this bit width. + * + * Specialized for each concrete alias: `STI_UINT128`, `STI_UINT160`, + * `STI_UINT192`, and `STI_UINT256`. + * + * @return The `SerializedTypeID` matching this instantiation's bit width. + */ [[nodiscard]] SerializedTypeID getSType() const override; + /** Return the hex-encoded string representation of the stored value. + * + * @return A lowercase hex string with no prefix. + */ [[nodiscard]] std::string getText() const override; + /** Test whether this field holds the same value as another `STBitString`. + * + * @param t The object to compare against. + * @return `true` if `t` is the same concrete bit width and both values + * are equal; `false` otherwise (including when `t` has a different + * bit width). + */ [[nodiscard]] bool isEquivalent(STBase const& t) const override; + /** Serialize the value into a `Serializer` byte buffer. + * + * Writes exactly `Bits/8` bytes to `s` via `Serializer::addBitString`. + * + * @param s The accumulator to write into. + * @pre `getFName().isBinary()` must be `true`. + * @pre `getFName().fieldType` must equal `getSType()`. + * @note Both preconditions are checked with `XRPL_ASSERT`; violations + * indicate a field/type metadata mismatch that would cause silent + * protocol corruption. + */ void add(Serializer& s) const override; + /** Return `true` when the stored value is the all-zeros bit string. + * + * The serialization layer uses this to decide whether a field may be + * omitted from canonical binary encoding. + * + * @return `true` if the value equals `beast::zero`. + */ [[nodiscard]] bool isDefault() const override; + /** Assign a new value, accepting any tag variant of `BaseUInt`. + * + * The free `Tag` template parameter allows cross-tag assignment (e.g., + * assigning a raw `uint256` to an `sfTransactionID` field) when the + * caller explicitly intends it, while making accidental mixing visible + * at the call site. Tag information is erased on storage. + * + * @tparam Tag The source tag type; any `BaseUInt` is accepted. + * @param v The new value. + */ template void setValue(BaseUInt const& v); + /** Return a const reference to the stored tag-free value. + * + * @return Reference to the internal `value_type`; valid for the lifetime + * of this object. + */ [[nodiscard]] value_type const& value() const; + /** Implicit conversion to the tag-free `value_type`. + * + * Erases any tag information from the underlying `BaseUInt` on the way + * out. Prefer `value()` in generic code to make the conversion explicit. + */ operator value_type() const; private: + /** Place-construct a copy into `buf` if it fits within `n` bytes; + * otherwise heap-allocate. Called by `detail::STVar` for the + * small-object optimisation inside `STObject` containers. + */ STBase* copy(std::size_t n, void* buf) const override; + + /** Place-construct a moved instance into `buf` if it fits within `n` + * bytes; otherwise heap-allocate. Called by `detail::STVar`. + */ STBase* move(std::size_t n, void* buf) override; friend class detail::STVar; }; +/** Serialized 128-bit opaque bit string (wire type `STI_UINT128`). */ using STUInt128 = STBitString<128>; + +/** Serialized 160-bit opaque bit string (wire type `STI_UINT160`). + * + * Used for `AccountID` fields and similar 20-byte identifiers. + */ using STUInt160 = STBitString<160>; + +/** Serialized 192-bit opaque bit string (wire type `STI_UINT192`). + * + * Used for `MPTID` fields (32-bit sequence number ‖ 160-bit issuer). + */ using STUInt192 = STBitString<192>; + +/** Serialized 256-bit opaque bit string (wire type `STI_UINT256`). + * + * The most commonly used alias; carries transaction hashes, ledger + * indices, node IDs, and other 32-byte protocol identifiers. + */ using STUInt256 = STBitString<256>; template diff --git a/include/xrpl/protocol/STBlob.h b/include/xrpl/protocol/STBlob.h index 84f44f1b78..dc2187e2f3 100644 --- a/include/xrpl/protocol/STBlob.h +++ b/include/xrpl/protocol/STBlob.h @@ -10,58 +10,180 @@ namespace xrpl { -// variable length byte string +/** Serialized-type representation of a variable-length binary field. + * + * `STBlob` backs any ledger or transaction field whose wire type is + * `STI_VL` (variable-length) **or** `STI_ACCOUNT` (20-byte account ID). + * Both types share identical VL-prefixed binary encoding on the wire; + * the semantic distinction is carried by the `SField` descriptor rather + * than this class. + * + * Storage is an owned `Buffer` (heap-backed `unique_ptr`). + * Read access is always through a non-owning `Slice`, keeping the + * ownership model explicit: holding a `Slice` confers no ownership claim. + * + * @note Do not store `STBlob` (or any `STBase`-derived type) in + * `std::vector` or other standard containers — `STBase::operator=` + * intentionally does not copy field names, which breaks slide-down + * semantics. Use `detail::STVar` (via `STObject`) instead. + */ class STBlob : public STBase, public CountedObject { Buffer value_; public: + /** Non-owning view type used for all read access to the payload. */ using value_type = Slice; STBlob() = default; + + /** Copy-construct, duplicating the owned byte buffer. */ STBlob(STBlob const& rhs); + /** Construct by copying @p size bytes from @p data into an owned buffer. + * + * @param f The `SField` descriptor identifying this field. + * @param data Pointer to the source bytes (must not be null if + * @p size is non-zero). + * @param size Number of bytes to copy. + */ STBlob(SField const& f, void const* data, std::size_t size); - STBlob(SField const& f, Buffer&& b); - STBlob(SField const& n); - STBlob(SerialIter&, SField const& name = kSF_GENERIC); + /** Construct by taking ownership of an existing buffer. + * + * @param f The `SField` descriptor identifying this field. + * @param b The buffer to move from; left empty after this call. + */ + STBlob(SField const& f, Buffer&& b); + + /** Construct an empty (default) blob associated with field @p n. + * + * `isDefault()` returns `true` until the payload is set. + * + * @param n The `SField` descriptor identifying this field. + */ + STBlob(SField const& n); + + /** Deserialize a variable-length blob from a byte stream. + * + * Reads the VL-prefixed byte sequence from @p st via + * `SerialIter::getVLBuffer()`. + * + * @param st Forward-only cursor over the serialized byte stream; + * advanced past the VL prefix and payload bytes on return. + * @param name The `SField` descriptor identifying this field. + */ + STBlob(SerialIter& st, SField const& name = kSF_GENERIC); + + /** Return the number of bytes in the payload. */ [[nodiscard]] std::size_t size() const; + /** Return a pointer to the first byte of the payload, or null if empty. */ [[nodiscard]] std::uint8_t const* data() const; + /** Return `STI_VL`, the wire type tag for variable-length fields. + * + * @note Returns `STI_VL` even when the associated `SField` has type + * `STI_ACCOUNT`. Both types share identical VL-prefixed encoding; + * the field-ID byte written by the enclosing `STObject` carries the + * semantic distinction. + */ [[nodiscard]] SerializedTypeID getSType() const override; + /** Return the payload as an uppercase hex string for logging and JSON output. */ [[nodiscard]] std::string getText() const override; + /** Serialize the payload into @p s with a VL length prefix. + * + * Writes the byte count followed by the raw bytes via + * `Serializer::addVL`, the exact inverse of the deserialization path. + * + * @param s The `Serializer` accumulator to append to. + * @note Asserts that the associated `SField` is a binary field and that + * its `fieldType` is `STI_VL` or `STI_ACCOUNT`. A field of any other + * type indicates a construction-time programming error and would + * produce a malformed wire encoding. + */ void add(Serializer& s) const override; + /** Return `true` if @p t is an `STBlob` with byte-identical content. + * + * Field names are not compared — only the raw payload bytes. + * + * @param t The other serialized-type object to compare against. + */ [[nodiscard]] bool isEquivalent(STBase const& t) const override; + /** Return `true` if this blob holds no bytes (empty buffer). + * + * `STObject` uses this to omit optional fields whose payload has not + * been set, keeping wire representations compact. + */ [[nodiscard]] bool isDefault() const override; + /** Replace the payload with a copy of @p slice. + * + * Allocates a fresh `Buffer` and copies the bytes from @p slice. + * Use `operator=(Buffer&&)` or `setValue(Buffer&&)` to transfer + * ownership without a copy. + * + * @param slice Non-owning view of the source bytes. + * @return `*this` + */ STBlob& operator=(Slice const& slice); + /** Return a non-owning view of the payload. + * + * The returned `Slice` is valid only for the lifetime of this `STBlob` + * and is invalidated by any mutation (`operator=`, `setValue`). + */ [[nodiscard]] value_type value() const noexcept; + /** Transfer ownership of @p buffer to this blob in O(1). + * + * @p buffer is left empty after this call. + * + * @param buffer The buffer whose ownership is transferred. + * @return `*this` + */ STBlob& operator=(Buffer&& buffer); + /** Transfer ownership of @p b to this blob in O(1). + * + * Named alternative to `operator=(Buffer&&)` for call sites where an + * explicit setter reads more clearly than an assignment expression. + * @p b is left empty after this call. + * + * @param b The buffer whose ownership is transferred. + */ void setValue(Buffer&& b); private: + /** Place-construct a copy into an `STVar` inline buffer or heap. + * + * Called exclusively by `detail::STVar`. Delegates to + * `STBase::emplace(n, buf, *this)`. + */ STBase* copy(std::size_t n, void* buf) const override; + + /** Place-construct a moved instance into an `STVar` inline buffer or heap. + * + * Called exclusively by `detail::STVar`. Delegates to + * `STBase::emplace(n, buf, std::move(*this))`, transferring `Buffer` + * ownership without copying the payload bytes. + */ STBase* move(std::size_t n, void* buf) override; diff --git a/include/xrpl/protocol/STCurrency.h b/include/xrpl/protocol/STCurrency.h index 55d1ab1e74..c97c18b019 100644 --- a/include/xrpl/protocol/STCurrency.h +++ b/include/xrpl/protocol/STCurrency.h @@ -1,3 +1,8 @@ +/** @file + * Defines `STCurrency`, the serialized-type wrapper for 160-bit XRPL + * currency identifiers used inside transactions and ledger objects. + */ + #pragma once #include @@ -8,6 +13,24 @@ namespace xrpl { +/** Serialized-type wrapper for a 160-bit XRPL currency identifier. + * + * `STCurrency` carries a `Currency` value (`base_uint<160, + * detail::CurrencyTag>`) inside the XRPL serialized-type field framework. + * Every field in a serialized transaction or ledger object must be an + * `STBase` subclass; `STCurrency` is the required adaptor for raw + * `Currency` values. + * + * The default (zero) value represents native XRP: `isDefault()` returns + * `true` whenever `isXRP(currency_)` is true, which causes the field to + * be omitted from canonical serialization when it carries no information + * beyond "this is XRP." + * + * Unlike `STAccount`, this class does not mix in `CountedObject`, so + * instance counts are not tracked for diagnostic purposes. + * + * @see STAccount, STIssue, Currency + */ class STCurrency final : public STBase { private: @@ -16,52 +39,164 @@ private: public: using value_type = Currency; + /** Construct an anonymous default (XRP) currency field. */ STCurrency() = default; + /** Deserialize a currency field from a binary wire stream. + * + * Reads exactly 160 bits from `sit` via `SerialIter::get160()`. No + * semantic validation is performed — binary data arriving from a + * consensus-validated ledger stream is assumed well-formed. + * + * @param sit Forward-only cursor over the serialized byte buffer; + * advanced by 20 bytes on return. + * @param name The `SField` descriptor for this field. + */ explicit STCurrency(SerialIter& sit, SField const& name); + /** Construct a currency field with a known value. + * + * The standard programmatic constructor used when the `Currency` is + * already available (e.g., when building a transaction in memory). + * + * @param name The `SField` descriptor for this field. + * @param currency The 160-bit currency identifier to store. + */ explicit STCurrency(SField const& name, Currency const& currency); + /** Construct a named but default (XRP) currency field. + * + * Binds the field to `name` and leaves the stored currency as the + * all-zeroes XRP value. Used when an `STObject` allocates a slot + * before the currency is known. + * + * @param name The `SField` descriptor for this field. + */ explicit STCurrency(SField const& name); + /** Return the stored 160-bit currency identifier. */ [[nodiscard]] Currency const& currency() const; + /** Return the stored 160-bit currency identifier. + * + * Alias for `currency()`, provided so generic code that expects a + * `value()` accessor on all ST wrapper types works uniformly. + */ [[nodiscard]] Currency const& value() const noexcept; + /** Replace the stored currency with `currency`. + * + * @param currency The new 160-bit currency identifier. + */ void setCurrency(Currency const& currency); + /** Return the `STI_CURRENCY` type tag used for field-dispatch and wire + * encoding. + */ [[nodiscard]] SerializedTypeID getSType() const override; + /** Return a human-readable currency string. + * + * Delegates to `to_string(currency_)`: returns `""` for XRP (zero), + * a three-character ISO-4217-style ticker (e.g., `"USD"`) for + * well-known tokens, or a hex string for opaque 160-bit custom + * currencies. + * + * @return String representation of the stored currency. + */ [[nodiscard]] std::string getText() const override; + /** Return a JSON string representation of the currency. + * + * The output is identical to `getText()`. The `JsonOptions` argument + * is ignored — a currency code is always a plain string with no + * optional decoration. + * + * @return JSON string value of the currency. + */ [[nodiscard]] json::Value getJson(JsonOptions) const override; + /** Append the currency to `s` as a raw 20-byte bit string. + * + * Writes the 160-bit value verbatim via `Serializer::addBitString`, + * with no VL prefix or other framing — consistent with all + * fixed-width scalar ST types. + * + * @param s The `Serializer` accumulator to append to. + */ void add(Serializer& s) const override; + /** Check semantic equivalence with another serialized field. + * + * Uses `dynamic_cast` to confirm `t` is also an `STCurrency`, then + * compares the stored 160-bit values. Returns `false` for any other + * `STBase` subtype. + * + * @param t The field to compare against. + * @return `true` if `t` is an `STCurrency` holding the same currency. + */ [[nodiscard]] bool isEquivalent(STBase const& t) const override; + /** Return `true` when the stored currency is XRP (all-zeroes). + * + * In `STBase` semantics, fields at their default value are omitted + * from canonical serialization. An `STCurrency` whose value is XRP + * need not carry an explicit currency code on the wire. + */ [[nodiscard]] bool isDefault() const override; private: + /** Factory called by `detail::STVar` when the wire type tag resolves + * to `STI_CURRENCY`. Delegates to the `SerialIter` constructor. + */ static std::unique_ptr construct(SerialIter&, SField const& name); + /** Place a copy of this object into `buf` (if it fits within `n` + * bytes) or heap-allocate via `STBase::emplace()`. + * Used by `detail::STVar` for the small-object optimization. + */ STBase* copy(std::size_t n, void* buf) const override; + + /** Place a moved instance into `buf` (if it fits within `n` bytes) + * or heap-allocate via `STBase::emplace()`. + * Used by `detail::STVar` for the small-object optimization. + */ STBase* move(std::size_t n, void* buf) override; friend class detail::STVar; }; +/** Parse and validate a currency field from a JSON value. + * + * Acts as the defensive input validation gateway for API-sourced + * currency strings. Unlike binary deserialization, which trusts + * consensus-validated data, this function validates strictly because + * JSON input arrives from untrusted API consumers. + * + * Accepts only JSON string values. The string is converted via + * `toCurrency()` and then checked against two sentinel values that + * `toCurrency()` may silently return for invalid input: + * - `noCurrency()` — syntactically invalid string. + * - `badCurrency()` — the string `"XRP"` used as an IOU ticker, which + * is explicitly prohibited to prevent confusion with native XRP. + * + * @param name The `SField` descriptor for the resulting field. + * @param v The JSON value to parse; must be a string. + * @return An `STCurrency` holding the validated, non-reserved currency. + * @throws std::runtime_error if `v` is not a string, or if the string + * resolves to `badCurrency()` or `noCurrency()`. + */ STCurrency currencyFromJson(SField const& name, json::Value const& v); @@ -83,30 +218,41 @@ STCurrency::setCurrency(Currency const& currency) currency_ = currency; } +/** Return `true` if both `STCurrency` objects hold the same 160-bit value. */ inline bool operator==(STCurrency const& lhs, STCurrency const& rhs) { return lhs.currency() == rhs.currency(); } +/** Return `true` if the two `STCurrency` objects hold different values. */ inline bool operator!=(STCurrency const& lhs, STCurrency const& rhs) { return !operator==(lhs, rhs); } +/** Less-than comparison between two `STCurrency` values, enabling use in + * sorted containers. + */ inline bool operator<(STCurrency const& lhs, STCurrency const& rhs) { return lhs.currency() < rhs.currency(); } +/** Return `true` if the `STCurrency` holds the same 160-bit value as `rhs`. + * + * Avoids constructing a temporary `STCurrency` when comparing a wrapped + * field directly against an unwrapped `Currency` value. + */ inline bool operator==(STCurrency const& lhs, Currency const& rhs) { return lhs.currency() == rhs; } +/** Less-than comparison between an `STCurrency` and a raw `Currency` value. */ inline bool operator<(STCurrency const& lhs, Currency const& rhs) { diff --git a/include/xrpl/protocol/STExchange.h b/include/xrpl/protocol/STExchange.h index c733df37cf..51b8bbcb1a 100644 --- a/include/xrpl/protocol/STExchange.h +++ b/include/xrpl/protocol/STExchange.h @@ -1,3 +1,15 @@ +/** @file + * Type-safe bridge between serialized (`STBase`-derived) types and native C++ + * types for reading and writing `STObject` fields. + * + * Application code works with plain C++ types (integers, `Slice`, `Buffer`), + * while the wire protocol stores everything in serialized form (`STInteger`, + * `STBlob`, etc.). The `STExchange` traits struct centralizes the conversion + * mappings so callers never need to perform manual `dynamic_cast` or construct + * heap-allocated serialized objects directly. The free functions `get`, `set`, + * and `erase` are the primary interface for field access on `STObject`. + */ + #pragma once #include @@ -17,23 +29,60 @@ namespace xrpl { -/** Convert between serialized type U and C++ type T. */ +/** Traits adapter that maps a serialized type @p U to a native C++ type @p T. + * + * Each specialization provides: + * - `value_type` — the canonical C++ representation for the serialized type. + * - `get(optional&, U const&)` — extracts a native value from the + * serialized object. + * - `set(field, T const&)` — constructs a heap-allocated serialized object + * ready for insertion into an `STObject`. + * + * All conversions are resolved at compile time; there is no runtime + * polymorphism in the type mapping itself. Adding support for a new C++ view + * of an existing wire type requires only a new specialization here — the + * serialization infrastructure is not touched. + * + * @tparam U The serialized type (e.g. `STInteger`, `STBlob`). + * @tparam T The desired native C++ type (e.g. `uint32_t`, `Slice`, `Buffer`). + */ template struct STExchange; +/** `STExchange` specialization covering the full family of integer types. + * + * A single partial specialization handles `STUInt8`, `STUInt16`, `STUInt32`, + * `STUInt64`, and `STInt32` uniformly. `get` extracts the integer via + * `STInteger::value()` and `set` constructs a new `STInteger` on the heap. + * + * @tparam U The underlying integer type (e.g. `uint32_t`, `uint64_t`). + * @tparam T The desired C++ integer type to convert to/from. + */ template struct STExchange, T> { explicit STExchange() = default; + /** The canonical C++ integer type for this serialized field. */ using value_type = U; + /** Populate @p t with the integer value stored in @p u. + * + * @param t Output optional to receive the extracted value. + * @param u The serialized integer object to read from. + */ static void get(std::optional& t, STInteger const& u) { t = u.value(); } + /** Construct a heap-allocated `STInteger` initialized to @p t. + * + * @param f The field descriptor for the new serialized object. + * @param t The integer value to store. + * @return Owning pointer to the newly constructed serialized integer. + */ static std::unique_ptr> set(SField const& f, T const& t) { @@ -41,19 +90,37 @@ struct STExchange, T> } }; +/** `STExchange` specialization for reading an `STBlob` field as a `Slice`. + * + * `Slice` is a non-owning view, so both `get` and `set` always copy the + * underlying bytes — `get` via `emplace(data, size)` and `set` via the + * `STBlob(field, data, size)` constructor. + */ template <> struct STExchange { explicit STExchange() = default; + /** Non-owning byte view. */ using value_type = Slice; + /** Populate @p t with a `Slice` pointing into a copy of @p u's bytes. + * + * @param t Output optional to receive the `Slice`. + * @param u The serialized blob object to read from. + */ static void get(std::optional& t, STBlob const& u) { t.emplace(u.data(), u.size()); } + /** Construct a heap-allocated `STBlob` by copying the bytes of @p t. + * + * @param f The field descriptor for the new serialized object. + * @param t The source byte view to copy into the blob. + * @return Owning pointer to the newly constructed `STBlob`. + */ static std::unique_ptr set(TypedField const& f, Slice const& t) { @@ -61,25 +128,53 @@ struct STExchange } }; +/** `STExchange` specialization for reading an `STBlob` field as a `Buffer`. + * + * `Buffer` owns its memory. The lvalue `set` overload copies bytes into the + * new `STBlob`; the rvalue `set` overload moves the `Buffer` directly into + * the `STBlob`, avoiding an extra heap allocation on hot paths that build + * transaction objects. + */ template <> struct STExchange { explicit STExchange() = default; + /** Owning byte container. */ using value_type = Buffer; + /** Populate @p t with a `Buffer` containing a copy of @p u's bytes. + * + * @param t Output optional to receive the `Buffer`. + * @param u The serialized blob object to read from. + */ static void get(std::optional& t, STBlob const& u) { t.emplace(u.data(), u.size()); } + /** Construct a heap-allocated `STBlob` by copying the bytes of @p t. + * + * @param f The field descriptor for the new serialized object. + * @param t The source buffer to copy into the blob. + * @return Owning pointer to the newly constructed `STBlob`. + */ static std::unique_ptr set(TypedField const& f, Buffer const& t) { return std::make_unique(f, t.data(), t.size()); } + /** Construct a heap-allocated `STBlob` by moving @p t's storage. + * + * Preferred over the lvalue overload when the caller no longer needs + * the `Buffer`, as it avoids an extra heap allocation. + * + * @param f The field descriptor for the new serialized object. + * @param t The source buffer to move into the blob. + * @return Owning pointer to the newly constructed `STBlob`. + */ static std::unique_ptr set(TypedField const& f, Buffer&& t) { @@ -89,7 +184,26 @@ struct STExchange //------------------------------------------------------------------------------ -/** Return the value of a field in an STObject as a given type. */ +/** Read a field from an `STObject` as native C++ type @p T. + * + * Uses `STObject::peekAtPField` (non-mutating — does not insert a default for + * absent fields) and checks two distinct absence conditions: a null pointer + * (the field was never registered in the object's schema) and + * `STI_NOTPRESENT` (the field exists in the schema but has been explicitly + * marked absent). A `dynamic_cast` failure on the non-null, present field + * indicates a programming error and throws rather than returning empty. + * + * @tparam T The desired native C++ type to extract (e.g. `Slice` or `Buffer` + * for an `STBlob` field). + * @tparam U The serialized field type, inferred from @p f. + * @param st The object to read from. + * @param f The typed field descriptor identifying the field. + * @return The field value, or `std::nullopt` if the field is absent. + * @throws std::runtime_error If the field is present but its dynamic type + * does not match @p U — this indicates a programming error. + * @see get(STObject const&, TypedField const&) for the type-inferring + * overload that avoids spelling out @p T explicitly. + */ /** @{ */ template std::optional @@ -110,6 +224,19 @@ get(STObject const& st, TypedField const& f) return t; } +/** Read a field from an `STObject`, inferring the native type from the field + * descriptor's `value_type`. + * + * This is the ergonomic default: callers write `get(st, sfSequence)` rather + * than `get(st, sfSequence)`. Use the explicit-`T` overload when a + * different C++ view of the same wire type is needed (e.g. reading an `STBlob` + * as `Slice` for temporary inspection vs. `Buffer` for ownership). + * + * @tparam U The serialized field type, inferred from @p f. + * @param st The object to read from. + * @param f The typed field descriptor identifying the field. + * @return The field value as `U::value_type`, or `std::nullopt` if absent. + */ template std::optional::value_type> get(STObject const& st, TypedField const& f) @@ -118,7 +245,21 @@ get(STObject const& st, TypedField const& f) } /** @} */ -/** Set a field value in an STObject. */ +/** Write a value into a field of an `STObject`. + * + * Uses `std::decay` to strip cv-qualifiers and references before selecting the + * `STExchange` specialization, and `std::forward` to preserve value category + * so the move-semantic `Buffer&&` overload fires when an rvalue is passed. + * + * @tparam U The serialized field type, inferred from @p f. + * @tparam T The native C++ value type, inferred from @p t. May be an rvalue + * reference to trigger move-optimized specializations (e.g. + * `STExchange::set(f, Buffer&&)`). + * @param st The object to write into. + * @param f The typed field descriptor identifying the field. + * @param t The value to store; forwarded to the appropriate `STExchange` + * specialization. + */ template void set(STObject& st, TypedField const& f, T&& t) @@ -126,7 +267,18 @@ set(STObject& st, TypedField const& f, T&& t) st.set(STExchange>::set(f, std::forward(t))); } -/** Set a blob field using an init function. */ +/** Write a blob field whose contents are populated by a callable. + * + * Constructs an `STBlob` of @p size bytes and invokes @p init to fill it + * in-place, avoiding an intermediate copy for large blobs. + * + * @tparam Init A callable with signature compatible with the `STBlob` + * in-place initialization constructor. + * @param st The object to write into. + * @param f The field descriptor for the blob field. + * @param size The desired byte length of the blob. + * @param init Callable invoked to populate the blob's storage. + */ template void set(STObject& st, TypedField const& f, std::size_t size, Init&& init) @@ -134,7 +286,16 @@ set(STObject& st, TypedField const& f, std::size_t size, Init&& init) st.set(std::make_unique(f, size, init)); } -/** Set a blob field from data. */ +/** Write a blob field from a raw pointer and length. + * + * Convenience overload for C-style interop. Copies @p size bytes from + * @p data into a newly constructed `STBlob`. + * + * @param st The object to write into. + * @param f The field descriptor for the blob field. + * @param data Pointer to the source bytes. + * @param size Number of bytes to copy. + */ template void set(STObject& st, TypedField const& f, void const* data, std::size_t size) @@ -142,7 +303,17 @@ set(STObject& st, TypedField const& f, void const* data, std::size_t siz st.set(std::make_unique(f, data, size)); } -/** Remove a field in an STObject. */ +/** Mark a field as absent in an `STObject` without removing it from the schema. + * + * Delegates to `STObject::makeFieldAbsent`, which sets the field's type to + * `STI_NOTPRESENT`. The field slot is retained in the object's declared + * schema but contributes nothing to the wire encoding or canonical + * serialization. + * + * @tparam U The serialized field type, inferred from @p f. + * @param st The object to modify. + * @param f The typed field descriptor identifying the field to erase. + */ template void erase(STObject& st, TypedField const& f) diff --git a/include/xrpl/protocol/STInteger.h b/include/xrpl/protocol/STInteger.h index 4e3c9a8923..d029d72e05 100644 --- a/include/xrpl/protocol/STInteger.h +++ b/include/xrpl/protocol/STInteger.h @@ -5,62 +5,208 @@ namespace xrpl { +/** Wraps a plain integer inside the XRPL Serialized Type (ST) framework. + * + * Every integer-valued field in a ledger entry, transaction, or metadata + * object — sequence numbers, flags, fees, timestamps, transaction types — + * is represented at runtime as one of the five concrete aliases defined + * below (`STUInt8`, `STUInt16`, `STUInt32`, `STUInt64`, `STInt32`). + * + * The generic template supplies all methods that behave identically across + * integer widths (`add`, `isDefault`, `isEquivalent`, `operator=`, + * `copy`/`move` plumbing). Per-type specializations in `STInteger.cpp` + * provide `getSType()`, `getText()`, `getJson()`, and the deserialization + * constructor; these carry semantic knowledge that varies per instantiation + * (e.g., mapping `sfTransactionResult` bytes to TER strings, or rendering + * `STUInt64` as a JSON string to avoid IEEE 754 precision loss). + * + * `CountedObject>` adds a lock-free per-instantiation + * instance counter, so the diagnostic system can report live `STUInt32` and + * `STUInt64` counts separately. + * + * @tparam Integer The underlying C++ integer type (e.g., `std::uint32_t`). + */ template class STInteger : public STBase, public CountedObject> { public: + /** The underlying integer type wrapped by this instantiation. */ using value_type = Integer; private: Integer value_; public: + /** Construct an anonymous field holding @p v. + * + * The field has no associated `SField` name (uses the generic placeholder). + * Prefer the two-argument constructor when the field will be stored in an + * `STObject`, so the protocol field identity is preserved. + * + * @param v Initial value. + */ explicit STInteger(Integer v); + + /** Construct a named field holding @p v. + * + * @param n The `SField` descriptor that identifies this field on the wire. + * @param v Initial value; defaults to zero. + */ STInteger(SField const& n, Integer v = 0); + + /** Deserialize from a wire byte stream. + * + * Reads exactly `sizeof(Integer)` bytes from @p sit in big-endian order. + * Full specializations in `STInteger.cpp` provide the correct `sitGet*()` + * call for each instantiation width. + * + * @param sit Forward byte-stream cursor; advanced by `sizeof(Integer)`. + * @param name The `SField` descriptor to bind to the new object. + * @throws ripple::STObject::InvalidField or similar if @p sit underruns. + */ STInteger(SerialIter& sit, SField const& name); + /** Return the `SerializedTypeID` tag for this integer width. + * + * Full specializations return `STI_UINT8`, `STI_UINT16`, `STI_UINT32`, + * `STI_UINT64`, or `STI_INT32` as appropriate for `Integer`. + */ [[nodiscard]] SerializedTypeID getSType() const override; + /** Render the value to JSON, with field-identity-aware formatting. + * + * Most instantiations emit the raw integer. Notable exceptions: + * - `STUInt8` on `sfTransactionResult` → short TER token string (e.g., + * `"tesSUCCESS"`); raw integer on unrecognized codes. + * - `STUInt16` on `sfLedgerEntryType`/`sfTransactionType` → registered + * format name string (e.g., `"AccountRoot"`, `"Payment"`). + * - `STUInt32` on `sfPermissionValue` → permission name string. + * - `STUInt64` → always a JSON *string* (never a number) to avoid IEEE 754 + * precision loss; hex by default, decimal if `SField::sMD_BaseTen` is set. + * + * @param options Rendering flags (e.g., `JsonOptions::Values::None`). + * @return A `json::Value` representation of this field. + */ [[nodiscard]] json::Value getJson(JsonOptions) const override; + /** Return a human-readable string representation of the value. + * + * Applies the same field-identity-aware logic as `getJson()`, but returns + * a `std::string` suitable for diagnostics and logs rather than a JSON + * value. `STUInt8` on `sfTransactionResult` yields the long-form human + * description; `STUInt16` on `sfLedgerEntryType`/`sfTransactionType` yields + * the registered name; all others yield a decimal string. + * + * @return Human-readable string for the field's value. + */ [[nodiscard]] std::string getText() const override; + /** Serialize the integer value into @p s. + * + * Calls `s.addInteger(value_)`, which writes `sizeof(Integer)` bytes in + * big-endian order. Two `XRPL_ASSERT` guards fire in debug builds: one + * confirms the field is marked binary (`isBinary()`), and one confirms + * the field's declared type tag matches `getSType()`. A failing assert + * indicates a mis-wired field definition. + * + * @param s The `Serializer` accumulator to write into. + */ void add(Serializer& s) const override; + /** Return `true` if the wrapped value equals zero. + * + * The ST framework uses this to omit optional fields whose value is the + * type default, keeping wire representations canonical and compact. + */ [[nodiscard]] bool isDefault() const override; + /** Return `true` if @p t holds the same concrete type and the same value. + * + * Uses `dynamic_cast` to guard against comparing, say, an `STUInt32` + * with an `STUInt64` that happen to share the same bit pattern. Field + * names are ignored; only values are compared. + * + * @param t The object to compare against. + * @return `true` if @p t is the same `STInteger` instantiation + * and holds the same wrapped value. + */ [[nodiscard]] bool isEquivalent(STBase const& t) const override; + /** Assign a new raw value, preserving the field identity. + * + * @param v The new value to store. + * @return `*this`. + */ STInteger& operator=(value_type const& v); + /** Return the wrapped value without implicit conversion. + * + * Prefer this over `operator Integer()` when the intent is an explicit + * read; it is clearer at the call site that a raw integer is being + * extracted. + */ [[nodiscard]] value_type value() const noexcept; + /** Replace the wrapped value. + * + * @param v The new value to store. + */ void setValue(Integer v); + /** Implicit conversion to the underlying integer type. + * + * Allows `STInteger` to be passed to functions expecting `T` without + * an explicit `.value()` call. Use `.value()` when clarity at the call + * site matters more than brevity. + */ operator Integer() const; private: + /** Copy this object into @p buf (or heap) via `STBase::emplace()`. + * + * Called exclusively by `detail::STVar` to implement copy construction + * with the small-object optimization. + */ STBase* copy(std::size_t n, void* buf) const override; + + /** Move this object into @p buf (or heap) via `STBase::emplace()`. + * + * Called exclusively by `detail::STVar` to implement move construction + * with the small-object optimization. + */ STBase* move(std::size_t n, void* buf) override; friend class xrpl::detail::STVar; }; +/** 8-bit unsigned serialized integer; used for `sfTransactionResult`. */ using STUInt8 = STInteger; + +/** 16-bit unsigned serialized integer; used for `sfLedgerEntryType` and `sfTransactionType`. */ using STUInt16 = STInteger; + +/** 32-bit unsigned serialized integer; the most common integer field width. */ using STUInt32 = STInteger; + +/** 64-bit unsigned serialized integer. + * + * Always rendered as a JSON string (never a JSON number) to avoid IEEE 754 + * precision loss. Fields annotated with `SField::sMD_BaseTen` render as + * decimal; all others render as lowercase hexadecimal. + */ using STUInt64 = STInteger; +/** 32-bit signed serialized integer. */ using STInt32 = STInteger; template diff --git a/include/xrpl/protocol/STIssue.h b/include/xrpl/protocol/STIssue.h index f5e1f61168..359a69c9c1 100644 --- a/include/xrpl/protocol/STIssue.h +++ b/include/xrpl/protocol/STIssue.h @@ -8,6 +8,25 @@ namespace xrpl { +/** Serialized representation of a fungible asset identifier (XRP, IOU, or MPT). + * + * `STIssue` is the canonical `STBase` subtype for embedding an `Asset` inside + * a ledger object or transaction field. It bridges the polymorphic serialization + * framework and the `Asset` variant that unifies all three asset species. + * + * The wire format is type-multiplexed without a separate tag byte: XRP is + * a single 160-bit all-zeros currency sentinel; IOU is a 160-bit currency + * followed by a 160-bit issuer AccountID; MPT is a 160-bit issuer AccountID + * followed by the 160-bit `noAccount()` sentinel and a 32-bit sequence. + * The `noAccount()` sentinel is the discriminator between IOU and MPT — it is + * otherwise an illegal issuer address and will never appear in real IOU data. + * + * The class is declared `final`; the `STBase` hierarchy is complete without + * further inheritance. `CountedObject` instruments construction and + * destruction for runtime diagnostics. + * + * @see Asset, Issue, MPTIssue + */ class STIssue final : public STBase, CountedObject { private: @@ -19,56 +38,155 @@ public: STIssue() = default; STIssue(STIssue const& rhs) = default; + /** Deserialize an STIssue from a byte stream, detecting XRP, IOU, or MPT. + * + * Reads a 160-bit slot. If all-zeros (XRP currency sentinel), the asset is + * XRP and deserialization is complete. Otherwise reads a second 160-bit slot: + * if it equals `noAccount()`, the asset is an MPT — a 32-bit sequence number + * follows and the three values are assembled into an `MPTID`. Any other + * second slot forms the `(currency, account)` pair of an IOU `Issue`. + * + * @param sit Forward cursor over the serialized byte buffer; advanced in place. + * @param name The SField identifying this field within its parent STObject. + * @throws std::runtime_error if an IOU's currency/account native-flag + * combination is invalid (e.g., XRP currency paired with a non-XRP account). + */ explicit STIssue(SerialIter& sit, SField const& name); + /** Construct an STIssue tagged to a specific field and holding the given asset. + * + * Accepts any type satisfying the `AssetType` concept (`Issue`, `MPTIssue`, + * `MPTID`, or `Asset`). For `Issue`-typed assets, the currency/account + * native-flag combination is validated via `isConsistent()`; MPT issuances + * are always considered consistent. + * + * @tparam A An `AssetType` — `Issue`, `MPTIssue`, `MPTID`, or `Asset`. + * @param name The SField that names this field within a parent STObject. + * @param issue The asset to wrap. + * @throws std::runtime_error if the asset is an `Issue` and its currency + * and account native flags are inconsistent. + */ template explicit STIssue(SField const& name, A const& issue); + /** Construct an XRP STIssue tagged to a specific field. + * + * Convenience constructor producing the default (XRP) asset bound to + * the given field name. Equivalent to `STIssue(name, xrpIssue())`. + * + * @param name The SField identifying this field within its parent STObject. + */ explicit STIssue(SField const& name); STIssue& operator=(STIssue const& rhs) = default; + /** Return the held asset as the concrete type `TIss`. + * + * @tparam TIss The requested type — `Issue` or `MPTIssue`. + * @return A const reference to the underlying `TIss` value. + * @throws std::runtime_error if the variant does not hold `TIss`. + */ template TIss const& get() const; + /** Return whether the held asset is of type `TIss`. + * + * @tparam TIss The type to query — `Issue` or `MPTIssue`. + * @return `true` if the underlying `Asset` variant holds `TIss`. + */ template [[nodiscard]] bool holds() const; + /** Return the underlying `Asset` without copying or throwing. + * + * @return A const reference to the wrapped `Asset`. + */ [[nodiscard]] value_type const& value() const noexcept; + /** Replace the held asset, re-running the consistency check for IOU issues. + * + * If the current asset is an `Issue`, `isConsistent()` is applied to the + * incoming value before assignment. MPT assets always pass through. + * + * @param issue The new asset to store. + * @throws std::runtime_error if `issue` is an `Issue` with mismatched + * native-flag and account. + */ void setIssue(Asset const& issue); + /** @return The serialized type identifier `STI_ISSUE` for generic field dispatch. */ [[nodiscard]] SerializedTypeID getSType() const override; + /** @return A human-readable string for the asset (e.g., `"XRP"`, + * `"USD/r..."`, or the raw hex of an MPTID). + */ [[nodiscard]] std::string getText() const override; + /** Serialize the asset to a JSON value suitable for RPC responses. + * + * @return A `json::Value` representing the asset, formatted per asset type. + */ [[nodiscard]] json::Value getJson(JsonOptions) const override; + /** Append the binary encoding of this asset to `s`. + * + * XRP: 160-bit zero currency sentinel only. + * IOU: 160-bit currency + 160-bit issuer AccountID. + * MPT: 160-bit issuer AccountID + 160-bit `noAccount()` sentinel + 32-bit sequence. + * + * @param s The Serializer to append bytes to. + */ void add(Serializer& s) const override; + /** Return `true` if `t` is an STIssue holding an equivalent asset. + * + * @param t The STBase to compare; downcast to STIssue internally. + * @return `true` if `t` is an STIssue whose asset compares equal to this one. + */ [[nodiscard]] bool isEquivalent(STBase const& t) const override; + /** Return `true` if this field holds the default asset (XRP). + * + * A field absent from a ledger object is implicitly XRP, so `xrpIssue()` + * is the canonical default. MPT issuances are never considered default. + * + * @return `true` iff the held asset is `xrpIssue()`. + */ [[nodiscard]] bool isDefault() const override; + /** Compare two STIssue objects for equality. Delegates to `Asset::operator==`. */ friend constexpr bool operator==(STIssue const& lhs, STIssue const& rhs); + /** Three-way comparison of two STIssue objects. Delegates to `Asset::operator<=>`. + * + * @note Across asset types, `MPTIssue` sorts before `Issue` (variant index order). + */ friend constexpr std::weak_ordering operator<=>(STIssue const& lhs, STIssue const& rhs); + /** Compare an STIssue directly to a raw Asset for equality. + * + * Avoids the need to unwrap the STIssue when comparing against an `Asset` + * value elsewhere in the engine. + */ friend constexpr bool operator==(STIssue const& lhs, Asset const& rhs); + /** Three-way comparison of an STIssue against a raw Asset. + * + * @note Across asset types, `MPTIssue` sorts before `Issue` (variant index order). + */ friend constexpr std::weak_ordering operator<=>(STIssue const& lhs, Asset const& rhs); @@ -88,6 +206,17 @@ STIssue::STIssue(SField const& name, A const& asset) : STBase{name}, asset_{asse Throw("Invalid asset: currency and account native mismatch"); } +/** Construct an STIssue by parsing a JSON asset representation. + * + * Delegates to `assetFromJson()` to resolve the JSON into the appropriate + * `Asset` variant (`xrpIssue()`, an IOU `Issue`, or an `MPTIssue`), then + * wraps it in an STIssue bound to `name`. This is the canonical entry point + * when deserializing an issue field from an API request. + * + * @param name The SField to attach to the resulting STIssue. + * @param v A JSON value encoding an asset (XRP object, IOU object, or MPT object). + * @return An STIssue bound to `name` holding the parsed asset. + */ STIssue issueFromJson(SField const& name, json::Value const& v); diff --git a/include/xrpl/protocol/STLedgerEntry.h b/include/xrpl/protocol/STLedgerEntry.h index aa87411ae6..b3b07b1e29 100644 --- a/include/xrpl/protocol/STLedgerEntry.h +++ b/include/xrpl/protocol/STLedgerEntry.h @@ -10,50 +10,176 @@ namespace test { class Invariants_test; } // namespace test +/** The C++ representation of a single object in the XRPL ledger state (universally aliased as `SLE`). + * + * Each ledger entry lives in a `SHAMap` keyed by a 256-bit `key_`. The + * `type_` member names what the object is (account root, offer, escrow, + * trust line, etc.) and determines which `SOTemplate` governs its field + * layout. Construction from a `Keylet` looks up the registered + * `LedgerFormats` schema and throws immediately if the type is unknown, + * ensuring that an SLE always has a valid, self-consistent type. + * + * Declared `final`: ledger-entry type diversity is handled entirely through + * the `LedgerFormats` registration system at runtime, not through C++ + * subclass hierarchies. + * + * @see SLE (alias below), Keylet, LedgerFormats + */ class STLedgerEntry final : public STObject, public CountedObject { uint256 key_; LedgerEntryType type_; public: + /** Shared-pointer to a mutable ledger entry. */ using pointer = std::shared_ptr; + /** Const reference to a shared-pointer to a mutable ledger entry. */ using ref = std::shared_ptr const&; + /** Shared-pointer to an immutable ledger entry. */ using const_pointer = std::shared_ptr; + /** Const reference to a shared-pointer to an immutable ledger entry. */ using const_ref = std::shared_ptr const&; - /** Create an empty object with the given key and type. */ + /** Construct a new, empty ledger entry for the given keylet. + * + * Looks up the `LedgerFormats` schema for `k.type`, applies the + * `SOTemplate` (populating all declared fields at their default values), + * and writes `sfLedgerEntryType` so the wire encoding is self-describing. + * + * @param k Keylet carrying the SHAMap key and the desired entry type. + * @throws std::runtime_error if `k.type` is not registered in + * `LedgerFormats`. + */ explicit STLedgerEntry(Keylet const& k); + + /** Convenience constructor that delegates to the `Keylet` path. + * + * Equivalent to `STLedgerEntry(Keylet(type, key))`. Prefer the + * `Keylet` overload where one is already available. + * + * @param type The ledger entry type. + * @param key SHAMap key for this entry. + * @throws std::runtime_error if `type` is not registered in + * `LedgerFormats`. + */ STLedgerEntry(LedgerEntryType type, uint256 const& key); + + /** Deserialize a ledger entry from a byte stream. + * + * Reads all fields from `sit` into the underlying `STObject`, then + * resolves `sfLedgerEntryType` to set `type_` and enforces the matching + * `SOTemplate` via `setSLEType()`. + * + * @param sit Wire-format byte cursor; consumed in place. + * @param index SHAMap key that addresses this entry in the ledger state. + * @throws std::runtime_error if the deserialized type is unrecognized or + * the field set does not conform to the declared template. + */ STLedgerEntry(SerialIter& sit, uint256 const& index); + + /** Convenience rvalue overload that forwards to the lvalue `SerialIter` constructor. + * + * `SerialIter` is consumed by position rather than by move semantics, so + * this overload simply binds the rvalue to an lvalue reference and + * delegates. + * + * @param sit Wire-format byte cursor; consumed in place. + * @param index SHAMap key that addresses this entry in the ledger state. + * @throws std::runtime_error if the deserialized type is unrecognized or + * the field set does not conform to the declared template. + */ STLedgerEntry(SerialIter&& sit, uint256 const& index); + + /** Promote a pre-populated `STObject` to a typed ledger entry. + * + * Used when fields have already been parsed into a generic `STObject` + * and need to be re-interpreted with a concrete `LedgerEntryType`. + * Delegates to `setSLEType()` for type resolution and template + * conformance. + * + * @param object The source object, copied into this entry. + * @param index SHAMap key that addresses this entry in the ledger state. + * @throws std::runtime_error if `sfLedgerEntryType` is absent, + * unrecognized, or the field set does not conform to the declared + * template. + */ STLedgerEntry(STObject const& object, uint256 const& index); + /** Return the serialized type identifier for ledger entries (`STI_LEDGERENTRY`). */ [[nodiscard]] SerializedTypeID getSType() const override; + /** Return a verbose diagnostic string containing the type name, key, and all field values. + * + * Re-validates `type_` against `LedgerFormats` as a defensive invariant + * check before emitting output. + * + * @throws std::runtime_error if `type_` is no longer recognized + * (indicates in-memory corruption). + */ [[nodiscard]] std::string getFullText() const override; + /** Return a compact diagnostic string containing the hex key and field contents. */ [[nodiscard]] std::string getText() const override; + /** Serialize this entry to JSON, augmenting the base `STObject` output. + * + * Injects `"index"` (the hex-encoded SHAMap key) because `key_` is not + * stored as a serialized field. For `ltMPTOKEN_ISSUANCE` objects, also + * injects `"mpt_issuance_id"` computed from `sfSequence` and `sfIssuer` + * — this derived identifier is not stored on-ledger and is recomputed + * on every read to keep consensus-critical storage non-redundant. + * + * @param options Controls JSON formatting (e.g., binary vs. human-readable). + * @return A `Json::Value` object with all fields plus the injected keys. + */ [[nodiscard]] json::Value getJson(JsonOptions options = JsonOptions::Values::None) const override; - /** Returns the 'key' (or 'index') of this item. - The key identifies this entry's position in - the SHAMap associative container. - */ + /** Return the 256-bit SHAMap key that locates this entry in the ledger state. */ [[nodiscard]] uint256 const& key() const; + /** Return the `LedgerEntryType` that identifies what kind of object this is. */ [[nodiscard]] LedgerEntryType getType() const; - // is this a ledger entry that can be threaded + /** Determine whether this entry participates in transaction threading. + * + * Threading links each ledger entry to the transaction that last modified + * it via `sfPreviousTxnID` / `sfPreviousTxnLgrSeq`. Five types + * (`ltDIR_NODE`, `ltAMENDMENTS`, `ltFEE_SETTINGS`, `ltNEGATIVE_UNL`, + * `ltAMM`) only gained `sfPreviousTxnID` support when the + * `fixPreviousTxnID` amendment activated; before that, this method + * returns `false` for those types even if the field technically exists in + * the template, preventing premature use of threading on objects that + * historically lacked it. + * + * @param rules The active amendment rules for the current ledger. + * @return `true` if this entry carries `sfPreviousTxnID` and threading + * is permitted under the current rules. + */ [[nodiscard]] bool isThreadedType(Rules const& rules) const; + /** Update the threading fields to record that `txID` last modified this entry. + * + * Reads the current `sfPreviousTxnID`; if it already equals `txID`, the + * transaction has already been applied to this entry — asserts that + * `sfPreviousTxnLgrSeq` also matches and returns `false` (idempotency + * guard against double-application). Otherwise writes `txID` and + * `ledgerSeq` into the object and captures the old values in the output + * parameters so callers can reconstruct the modification chain. + * + * @param txID Hash of the transaction that is modifying this entry. + * @param ledgerSeq Sequence number of the ledger containing `txID`. + * @param prevTxID [out] The previous value of `sfPreviousTxnID`. + * @param prevLedgerID [out] The previous value of `sfPreviousTxnLgrSeq`. + * @return `true` if the fields were updated; `false` if `txID` was + * already threaded and the entry is unchanged. + */ bool thread( uint256 const& txID, @@ -62,9 +188,17 @@ public: std::uint32_t& prevLedgerID); private: - /* Make STObject comply with the template for this SLE type - Can throw - */ + /** Resolve `type_` from the embedded `sfLedgerEntryType` field and enforce + * template conformance on an already-populated object. + * + * Post-hoc counterpart to the `set(SOTemplate)` call in the `Keylet` + * constructor: instead of initializing an empty object, it validates + * and conforms an already-populated one. Called after wire + * deserialization and after `STObject` promotion. + * + * @throws std::runtime_error if `sfLedgerEntryType` names an unrecognized + * type, or if `applyTemplate()` rejects the field set. + */ void setSLEType(); @@ -79,6 +213,7 @@ private: friend class detail::STVar; }; +/** Canonical short alias for `STLedgerEntry`, used pervasively throughout the codebase. */ using SLE = STLedgerEntry; inline STLedgerEntry::STLedgerEntry(LedgerEntryType type, uint256 const& key) @@ -93,10 +228,6 @@ inline STLedgerEntry::STLedgerEntry( { } -/** Returns the 'key' (or 'index') of this item. - The key identifies this entry's position in - the SHAMap associative container. -*/ inline uint256 const& STLedgerEntry::key() const { diff --git a/include/xrpl/protocol/STNumber.h b/include/xrpl/protocol/STNumber.h index 8594a292f4..e2749aea2b 100644 --- a/include/xrpl/protocol/STNumber.h +++ b/include/xrpl/protocol/STNumber.h @@ -9,27 +9,26 @@ namespace xrpl { -/** - * A serializable number. +/** Serializable asset-contextual numeric field for XRPL ledger objects. * - * This type is-a `Number`, and can be used everywhere that is accepted. - * This type simply integrates `Number` with the serialization framework, - * letting it be used for fields in ledger entries and transactions. - * It is effectively an `STAmount` sans `Asset`: - * it can represent a value of any token type (XRP, IOU, or MPT) - * without paying the storage cost of duplicating asset information - * that may be deduced from the context. + * `STNumber` is effectively an `STAmount` without embedded `Asset` metadata. + * It stores only a `Number` (signed 64-bit mantissa + 32-bit exponent, 12 + * bytes on the wire) and defers asset identity to runtime via the + * `STTakesAsset` mixin. This eliminates the per-field storage cost of + * duplicating asset information that is already present in the containing + * ledger entry (Vault, LoanBroker, Loan). All `NUMBER`-type SFields carry + * the `sMD_NeedsAsset` metadata flag; the free function + * `associateAsset(STLedgerEntry&, Asset const&)` walks a ledger entry and + * calls `associateAsset` on each such field near the end of `doApply()`. * - * STNumber derives from STTakesAsset, so that it can be associated with the - * related Asset during transaction processing. Which asset is relevant depends - * on the object and transaction. As of this writing, only Vault, LoanBroker, - * and Loan objects use STNumber fields. All of those fields represent amounts - * of the Vault's Asset, so they should be associated with the Vault's Asset. + * Because `STNumber` provides `operator Number() const`, it can be passed + * directly wherever a `Number` is expected. * - * e.g. - * associateAsset(*loanSle, asset); - * associateAsset(*brokerSle, asset); - * associateAsset(*vaultSle, asset); + * @note After `associateAsset()` is called, the stored value is rounded to + * the asset's canonical precision. Calling `setValue()` afterward + * without re-associating violates the two-phase rounding contract and + * will trigger an assertion in `add()`. + * @see STTakesAsset, STAmount, associateAsset(STLedgerEntry&, Asset const&) */ class STNumber : public STTakesAsset, public CountedObject { @@ -40,21 +39,72 @@ public: using value_type = Number; STNumber() = default; + + /** Construct an STNumber bound to the given SField with an initial value. + * + * @param field The SField that identifies this value in its containing + * object. Must have `fieldType == STI_NUMBER`. + * @param value Initial numeric value; defaults to `Number()` (zero with + * sentinel exponent `std::numeric_limits::lowest()`). + */ explicit STNumber(SField const& field, Number const& value = Number()); + + /** Deserialize an STNumber from a byte stream. + * + * Reads a 64-bit signed mantissa and a 32-bit signed exponent (12 bytes + * total) from @p sit. The two reads are issued as separate statements to + * guarantee evaluation order — merging them into a single call expression + * would produce undefined behavior because C++ does not sequence function + * arguments. + * + * @param sit Forward cursor positioned at the first byte of the payload. + * @param field The SField that identifies this value in its containing + * object. + */ STNumber(SerialIter& sit, SField const& field); + /** @return `STI_NUMBER`. */ [[nodiscard]] SerializedTypeID getSType() const override; + + /** @return Decimal string representation of the stored `Number`. */ [[nodiscard]] std::string getText() const override; + + /** Serialize the stored value as 12 bytes (int64 mantissa, int32 exponent). + * + * For `sMD_NeedsAsset` fields this is Phase 2 of the two-phase rounding + * contract. When an asset has been associated, the value is re-rounded and + * asserted equal to the stored value, confirming that `associateAsset()` + * was called after the last `setValue()`. When no asset is present, a + * debug-only assertion verifies that `MantissaRange::Large` is active, + * because serializing under the small mantissa scale would silently + * truncate XRP/MPT integer values larger than 15 digits. + * + * @param s Serializer accumulator to append to. + */ void add(Serializer& s) const override; + /** @return Read-only reference to the stored `Number`. */ [[nodiscard]] Number const& value() const; + + /** Replace the stored value without re-associating an asset. + * + * @param v New value. + * @note If `associateAsset()` has already been called on this field, + * calling `setValue()` afterward without re-associating violates the + * two-phase rounding contract and will trigger an assertion in `add()`. + */ void setValue(Number const& v); + /** Assign a new value; delegates to `setValue()`. + * + * @param rhs New value. + * @return `*this`. + */ STNumber& operator=(Number const& rhs) { @@ -62,14 +112,38 @@ public: return *this; } + /** @return `true` if the other `STBase` is an `STNumber` holding the same + * `Number` value. + */ [[nodiscard]] bool isEquivalent(STBase const& t) const override; + + /** @return `true` if the stored value equals the default-constructed + * `Number()` (zero with its sentinel exponent), ensuring zero-valued + * fields round-trip correctly without false positives. + */ [[nodiscard]] bool isDefault() const override; + /** Bind an asset and immediately round the stored value to its precision. + * + * Phase 1 of the two-phase rounding contract. Stores @p a via + * `STTakesAsset::associateAsset` then calls `roundToAsset(a, value_)`. + * For XRP and MPT this truncates fractional drops; for IOU this normalises + * to 15 significant decimal digits. After this call, `add()` will assert + * idempotency on the rounded value. + * + * @param a Asset whose precision governs rounding. + * @note The field must carry the `sMD_NeedsAsset` metadata flag; a debug + * assertion fires if it does not. + */ void associateAsset(Asset const& a) override; + /** Implicit conversion to `Number`, enabling use in numeric expressions. + * + * @return A copy of the stored `Number`. + */ operator Number() const { return value_; @@ -82,19 +156,68 @@ private: move(std::size_t n, void* buf) override; }; +/** Write the decimal string representation of @p rhs to @p out. + * + * @param out Output stream to write to. + * @param rhs The value to render. + * @return @p out. + */ std::ostream& operator<<(std::ostream& out, STNumber const& rhs); +/** Raw parsed components of a decimal number string. + * + * Produced by `partsFromString()` before normalization. The mantissa is + * always unsigned; sign is carried separately in `negative`. + */ struct NumberParts { + /** Unsigned integer formed by concatenating integer and fractional digits. */ std::uint64_t mantissa = 0; + /** Exponent adjusted for fractional digit count and any explicit `e` suffix. */ int exponent = 0; + /** `true` if the original string had a leading `'-'`. */ bool negative = false; }; +/** Parse a decimal string into its raw mantissa/exponent/sign components. + * + * Accepts an optional leading sign, a non-empty integer part (no leading + * zeroes unless the value is exactly `"0"`), an optional fractional part, + * and an optional `e`/`E` exponent suffix. No normalization is applied — + * the caller receives the raw parsed representation. + * + * @param number Decimal string to parse (e.g., `"3.14e2"`, `"-42"`, `"0"`). + * @return `NumberParts` with unsigned mantissa, adjusted exponent, and sign. + * @throws std::runtime_error if @p number does not match the expected format + * (e.g., empty string, leading zeroes, bare `"e"`, trailing decimal point). + * @throws std::bad_cast (via `boost::lexical_cast`) if the digit string + * overflows `uint64_t`. + * @note The backing regex is compiled once as a `static` local with the + * `optimize` flag to amortize construction cost across calls. + */ NumberParts partsFromString(std::string const& number); +/** Construct an STNumber from a JSON integer or decimal string. + * + * Dispatches on the JSON value type: + * - **Integer** (`isInt`/`isUInt`): reads the native integer value directly. + * - **String**: delegates to `partsFromString()`; this path asserts that no + * active transaction rules are present, restricting use to pre-transactor + * JSON deserialization (e.g., `STParsedJSON`). + * - Anything else throws. + * + * @param field The SField that identifies the resulting STNumber. + * @param value JSON node containing the numeric value. + * @return A new STNumber holding the parsed value, not yet asset-rounded. + * @throws std::runtime_error if @p value is not an integer or string, or if + * a string fails to parse as a valid decimal. + * @throws std::bad_cast (via `boost::lexical_cast`) if a string mantissa + * overflows `uint64_t`. + * @note String-format numbers are forbidden during active transaction + * processing; only numeric JSON types are accepted in that context. + */ STNumber numberFromJson(SField const& field, json::Value const& value); diff --git a/include/xrpl/protocol/STObject.h b/include/xrpl/protocol/STObject.h index c635e8ce22..2d7cab6e85 100644 --- a/include/xrpl/protocol/STObject.h +++ b/include/xrpl/protocol/STObject.h @@ -1,3 +1,15 @@ +/** @file + * Defines `STObject`, the heterogeneous field container that underlies every + * XRPL transaction, ledger entry, and inner object. + * + * `STObject` supports two operating modes: *free mode* (no schema, insertion + * order preserved) and *template mode* (schema enforced via `SOTemplate`, + * O(1) field lookup). Both `STTx` and `STLedgerEntry` are `final` subclasses. + * + * The proxy system (`ValueProxy`, `OptionalProxy`) provides type-safe, + * compile-time-checked field access via `operator[]` and `at()`, replacing + * the older `getFieldU32()`/`setFieldU32()` family for new code. + */ #pragma once #include @@ -27,15 +39,39 @@ namespace xrpl { class STArray; +/** Throw a `std::runtime_error` indicating a missing field. + * + * Used by the legacy typed-accessor family (`getFieldByValue`, + * `getFieldByConstRef`, `setFieldUsingSetValue`) when `peekAtPField` returns + * null. Not intended for direct use by callers outside `STObject`. + * + * @param field The field that could not be found. + * @throws std::runtime_error always. + */ inline void throwFieldNotFound(SField const& field) { Throw("Field not found: " + field.getName()); } +/** Heterogeneous, field-keyed container for XRPL protocol objects. + * + * Stores an ordered sequence of `STBase`-derived fields, keyed by `SField`. + * Operates in one of two modes: + * - *Free mode* (`isFree() == true`): no schema; fields are stored in + * insertion order and any field may be added. + * - *Template mode* (`isFree() == false`): an `SOTemplate` constrains + * which fields are present, enforces `soeREQUIRED`/`soeOPTIONAL`/ + * `soeDEFAULT` semantics, and enables O(1) field lookup. + * + * `STTx` and `STLedgerEntry` are the primary `final` subclasses. + * Field access is available via the modern proxy API (`operator[]`, `at()`) + * or the legacy typed-accessor family (`getFieldU32()`, etc.). + * + * @note `operator==` compares only wire-representable (`isBinary()`) fields. + */ class STObject : public STBase, public CountedObject { - // Proxy value for a STBase derived class template class Proxy; template @@ -43,6 +79,7 @@ class STObject : public STBase, public CountedObject template class OptionalProxy; + /** Functor used by `boost::transform_iterator` to project `STVar→STBase`. */ struct Transform { explicit Transform() = default; @@ -60,11 +97,22 @@ class STObject : public STBase, public CountedObject SOTemplate const* type_{}; public: + /** Forward iterator over the fields of this object as `STBase const&`. */ using iterator = boost::transform_iterator; ~STObject() override = default; STObject(STObject const&) = default; + /** Construct a templated `STObject` from a schema, field name, and + * an initializer callable. + * + * Delegates to `STObject(type, name)` then calls `f(*this)`, allowing + * fields to be populated inline at construction time. + * + * @param type The SOTemplate that defines the object's layout. + * @param name The SField identifying this object within its parent. + * @param f Callable `void(STObject&)` invoked after template init. + */ template STObject(SOTemplate const& type, SField const& name, F&& f) : STObject(type, name) { @@ -73,128 +121,328 @@ public: STObject& operator=(STObject const&) = default; + /** Move-construct, transferring field storage and template pointer. */ STObject(STObject&&); + /** Move-assign, transferring field storage and template pointer. */ STObject& operator=(STObject&& other); + /** Construct a templated `STObject` pre-populated from a schema. + * + * Every slot in `type` is initialized: `soeREQUIRED` fields receive their + * type-default value; `soeOPTIONAL` and `soeDEFAULT` fields receive the + * `STI_NOTPRESENT` sentinel. + * + * @param type The SOTemplate that defines the object's layout. + * @param name The SField identifying this object within its parent. + */ STObject(SOTemplate const& type, SField const& name); + + /** Construct a templated `STObject` by deserializing from a byte stream. + * + * Reads fields in free mode from `sit`, then calls `applyTemplate(type)` + * to reorder and validate them against the schema. + * + * @param type The SOTemplate to enforce after deserialization. + * @param sit The byte stream to deserialize from. + * @param name The SField identifying this object within its parent. + * @throws FieldErr if a required field is missing or an unknown + * non-discardable field is present. + */ STObject(SOTemplate const& type, SerialIter& sit, SField const& name); + + /** Construct a free-mode `STObject` by deserializing from a byte stream. + * + * The `depth` parameter guards against stack exhaustion when parsing + * deeply nested structures from untrusted input; nesting beyond 10 + * throws `std::runtime_error`. + * + * @param sit The byte stream to deserialize from. + * @param name The SField identifying this object within its parent. + * @param depth Current nesting depth (default 0); capped at 10. + * @throws std::runtime_error if depth exceeds 10 or data is malformed. + */ STObject(SerialIter& sit, SField const& name, int depth = 0); + + /** Construct from an rvalue `SerialIter`; delegates to the lvalue overload. */ STObject(SerialIter&& sit, SField const& name); + + /** Construct a free-mode (schema-less) `STObject` with the given field name. */ explicit STObject(SField const& name); + /** Create a free-mode inner object, conditionally binding a schema template. + * + * Checks the ambient `getCurrentTransactionRules()` to determine whether + * the `fixInnerObjTemplate` or `fixInnerObjTemplate2` amendments are + * active, and applies the corresponding `SOTemplate` from + * `InnerObjectFormats` when they are. This amendment-gated behaviour + * preserves replay compatibility with historical ledger data serialized + * before schemas existed. + * + * @param name The SField identifying the type of inner object to create. + * @return A new `STObject`, bound to its schema when the active rules permit. + */ static STObject makeInnerObject(SField const& name); + /** Return an iterator to the first field in this object. */ [[nodiscard]] iterator begin() const; + /** Return a past-the-end iterator for this object's fields. */ [[nodiscard]] iterator end() const; + /** Return `true` when this object contains no fields. */ [[nodiscard]] bool empty() const; + /** Reserve storage for at least `n` fields in the underlying vector. */ void reserve(std::size_t n); + /** Validate and reorder fields against a schema after free-mode deserialization. + * + * Rebuilds the internal storage in template order. `soeREQUIRED` fields + * that are missing throw; `soeDEFAULT` fields whose serialized value equals + * the type's zero value are rejected (explicit defaults are forbidden); + * unknown non-discardable fields throw. + * + * @param type The SOTemplate to enforce. + * @throws FieldErr on required-field missing, explicit default value, or + * unknown non-discardable field. + */ void applyTemplate(SOTemplate const& type); + /** Look up and apply the schema registered for `sField` in `InnerObjectFormats`. + * + * No-op when no template is registered for the given field. + * + * @param sField The SField whose registered SOTemplate should be applied. + * @throws FieldErr (from `applyTemplate`) if the object does not conform. + */ void applyTemplateFromSField(SField const&); + /** Return `true` when no schema template is associated with this object. */ [[nodiscard]] bool isFree() const; + /** Initialize this object from a template, pre-populating every slot. + * + * Clears existing fields and rebuilds in template order. `soeREQUIRED` + * fields receive their type-default value; all other fields receive the + * `STI_NOTPRESENT` sentinel. + * + * @param type The SOTemplate that defines the layout of this object. + */ void set(SOTemplate const&); + /** Deserialize fields from a byte stream into this object (free mode). + * + * Reads `(type, field)` ID pairs from `u`, constructs each child `STVar` + * at `depth+1`, and calls `applyTemplateFromSField()` on nested + * `STObject` children. Stops at an inner-object terminator byte. + * + * @param u The byte stream to read from. + * @param depth Current nesting depth; guards against deeply nested input. + * @return `true` if an inner-object terminator was consumed; `false` + * at top-level end-of-stream. + * @throws std::runtime_error on malformed data or duplicate fields. + */ bool set(SerialIter& u, int depth = 0); + /** Return `STI_OBJECT`. */ [[nodiscard]] SerializedTypeID getSType() const override; + /** Return `true` when the objects have the same wire-representable fields. + * + * Only fields where `SField::isBinary()` is true participate in the + * comparison. Non-binary (JSON-only) fields are ignored. + * + * @param t The other serialized type to compare against. + */ [[nodiscard]] bool isEquivalent(STBase const& t) const override; + /** Return `true` when this object holds no fields (empty storage). */ [[nodiscard]] bool isDefault() const override; + /** Serialize all fields (including signing fields) into `s`. */ void add(Serializer& s) const override; + /** Return a human-readable, field-by-field description of this object. */ [[nodiscard]] std::string getFullText() const override; + /** Return a brief textual description of this object. */ [[nodiscard]] std::string getText() const override; // TODO(tom): options should be an enum. + /** Convert this object to a JSON value. + * + * @param options Controls formatting options (e.g., binary vs. human-readable). + */ [[nodiscard]] json::Value getJson(JsonOptions = JsonOptions::Values::None) const override; + /** Serialize signing-eligible fields only into `s`. + * + * Excludes fields whose `SField::shouldInclude(false)` returns `false` + * (e.g., `sfTxnSignature`, `sfSigners`). Used to produce the payload + * that is hashed and signed or verified. + * + * @param s The serializer to append to. + */ void addWithoutSigningFields(Serializer& s) const; + /** Return a `Serializer` containing all fields (including signing fields). + * + * @note Produces a full copy of the serialized form; prefer + * `add(Serializer&)` when appending to an existing buffer. + */ [[nodiscard]] Serializer getSerializer() const; + /** Append a new field directly to the internal storage vector. + * + * Used during deserialization and by the proxy system when adding new + * fields to a free-mode object. Arguments are forwarded to + * `detail::STVar`'s constructor. + * + * @return The zero-based index of the newly added field. + */ template std::size_t emplaceBack(Args&&... args); + /** Return the number of fields currently stored in this object. */ [[nodiscard]] int getCount() const; + /** Set a flag bit in `sfFlags`, creating the field if absent. + * + * @param flag The flag bit(s) to set (OR-ed into existing flags). + * @return `true` if the flags field was changed. + */ bool setFlag(std::uint32_t); + + /** Clear a flag bit in `sfFlags`. + * + * @param flag The flag bit(s) to clear. + * @return `true` if the flags field was changed. + */ bool clearFlag(std::uint32_t); + + /** Return `true` when all bits in `flag` are set in `sfFlags`. */ [[nodiscard]] bool isFlag(std::uint32_t) const; + /** Return the value of `sfFlags`, or 0 if the field is absent. */ [[nodiscard]] std::uint32_t getFlags() const; + /** Compute a domain-separated hash of all fields (including signing fields). + * + * Prepends `prefix` before serializing all fields, then returns the + * `sha512Half` of the result. + * + * @param prefix The `HashPrefix` discriminator for this hash domain. + * @return The 256-bit hash. + */ [[nodiscard]] uint256 getHash(HashPrefix prefix) const; + /** Compute a domain-separated hash of signing-eligible fields only. + * + * Equivalent to `getHash` but uses `addWithoutSigningFields` to exclude + * signature-carrying fields. Used by single-sig and multi-sig verification. + * + * @param prefix The `HashPrefix` discriminator for this hash domain. + * @return The 256-bit hash. + */ [[nodiscard]] uint256 getSigningHash(HashPrefix prefix) const; + /** Return the field at `offset` by const reference; no bounds check. */ [[nodiscard]] STBase const& peekAtIndex(int offset) const; + /** Return the field at `offset` by mutable reference; no bounds check. */ STBase& getIndex(int offset); + /** Return a const pointer to the field at `offset`; no bounds check. */ [[nodiscard]] STBase const* peekAtPIndex(int offset) const; + /** Return a mutable pointer to the field at `offset`; no bounds check. */ STBase* getPIndex(int offset); + /** Return the storage index of `field`, or -1 if not present. */ [[nodiscard]] int getFieldIndex(SField const& field) const; + /** Return the `SField` descriptor for the field at storage index `index`. */ [[nodiscard]] SField const& getFieldSType(int index) const; + /** Return the field identified by `field` by const reference. + * + * @throws std::runtime_error if the field is not present. + */ [[nodiscard]] STBase const& peekAtField(SField const& field) const; + /** Return the field identified by `field` by mutable reference. + * + * @throws std::runtime_error if the field is not present. + */ STBase& getField(SField const& field); + /** Return a const pointer to the field identified by `field`, or `nullptr`. + * + * Does not create the field. Returns `nullptr` for absent optional fields + * in free mode; returns a pointer to the `STI_NOTPRESENT` sentinel in + * template mode. + */ [[nodiscard]] STBase const* peekAtPField(SField const& field) const; + /** Core field-lookup primitive, optionally creating the field. + * + * In template mode with `createOkay == false`, returns the stored slot + * pointer (which may be an `STI_NOTPRESENT` sentinel). With + * `createOkay == true`, promotes `STI_NOTPRESENT` sentinels and + * appends missing free-mode fields. Returns `nullptr` only when the + * field is absent in free mode and `createOkay` is false. + * + * @param field The field to locate. + * @param createOkay When `true`, create the field if absent. + * @return Pointer to the stored `STBase`, or `nullptr`. + */ STBase* getPField(SField const& field, bool createOkay = false); - // these throw if the field type doesn't match, or return default values - // if the field is optional but not present + /** @name Legacy typed field accessors + * + * These methods return field values by their native C++ type. They throw + * `std::runtime_error` if the field type does not match the expected type, + * and return a default-constructed value when an optional field is absent. + * Prefer the proxy API (`operator[]`, `at()`) for new code. + * @{ + */ [[nodiscard]] unsigned char getFieldU8(SField const& field) const; [[nodiscard]] std::uint16_t @@ -205,7 +453,6 @@ public: getFieldU64(SField const& field) const; [[nodiscard]] uint128 getFieldH128(SField const& field) const; - [[nodiscard]] uint160 getFieldH160(SField const& field) const; [[nodiscard]] uint192 @@ -216,7 +463,6 @@ public: getFieldI32(SField const& field) const; [[nodiscard]] AccountID getAccountID(SField const& field) const; - [[nodiscard]] Blob getFieldVL(SField const& field) const; [[nodiscard]] STAmount const& @@ -225,7 +471,12 @@ public: getFieldPathSet(SField const& field) const; [[nodiscard]] STVector256 const& getFieldV256(SField const& field) const; - // If not found, returns an object constructed with the given field + /** Return the nested `STObject` for `field` by value. + * + * If the field is absent, returns a default-constructed `STObject` + * initialized with `field` as its name. Modifications to the returned + * copy do not propagate back; use `peekFieldObject()` for in-place access. + */ [[nodiscard]] STObject getFieldObject(SField const& field) const; [[nodiscard]] STArray const& @@ -234,6 +485,7 @@ public: getFieldCurrency(SField const& field) const; [[nodiscard]] STNumber const& getFieldNumber(SField const& field) const; + /** @} */ /** Get the value of a field. @param A TypedField built from an SField value representing the desired @@ -329,15 +581,31 @@ public: OptionalProxy at(OptionaledField const& of); - /** Set a field. - if the field already exists, it is replaced. - */ + /** Replace or insert a field from a heap-allocated `STBase`. + * + * If a field with the same `SField` already exists, it is replaced. + * + * @param v The field to store; ownership is transferred. + */ void set(std::unique_ptr v); + /** Replace or insert a field by move. + * + * If a field with the same `SField` already exists, it is replaced. + * + * @param v The field to move into this object. + */ void set(STBase&& v); + /** @name Legacy typed field mutators + * + * Set a named field to the given value. Throws `std::runtime_error` if + * the stored field has a different type than the value being set. + * Prefer the proxy API (`operator[]`, `at()`) for new code. + * @{ + */ void setFieldU8(SField const& field, unsigned char); void @@ -358,10 +626,8 @@ public: setFieldVL(SField const& field, Blob const&); void setFieldVL(SField const& field, Slice const&); - void setAccountID(SField const& field, AccountID const&); - void setFieldAmount(SField const& field, STAmount const&); void @@ -379,40 +645,122 @@ public: void setFieldObject(SField const& field, STObject const& v); + /** Set a 160-bit hash field from any `BaseUInt<160, Tag>` value. + * + * @tparam Tag The phantom tag type of the `BaseUInt` specialization. + * @param field The field to set. + * @param v The 160-bit value to store. + * @throws std::runtime_error if the stored field has a different type. + */ template void setFieldH160(SField const& field, BaseUInt<160, Tag> const& v); + /** @} */ + /** Return a mutable reference to the nested `STObject` for `field`. + * + * Unlike `getFieldObject()`, the returned reference is into internal + * storage; mutations propagate back to this object. + * + * @throws std::runtime_error if the field is absent or has the wrong type. + */ STObject& peekFieldObject(SField const& field); + + /** Return a mutable reference to the nested `STArray` for `field`. + * + * The returned reference is into internal storage; mutations propagate + * back to this object. + * + * @throws std::runtime_error if the field is absent or has the wrong type. + */ STArray& peekFieldArray(SField const& field); + /** Return `true` when `field` is present and not an `STI_NOTPRESENT` sentinel. */ [[nodiscard]] bool isFieldPresent(SField const& field) const; + + /** Promote an `STI_NOTPRESENT` sentinel field to a live default value. + * + * In template mode, converts an optional/default slot from the sentinel + * state to a type-correct default value, making the field "present". + * In free mode, appends a new default-constructed field. + * + * @param field The field to make present. + * @return Pointer to the now-live field, or `nullptr` if not found. + */ STBase* makeFieldPresent(SField const& field); + + /** Demote a live optional field back to the `STI_NOTPRESENT` sentinel. + * + * In template mode, replaces the field's value with the sentinel. + * In free mode, removes the field entry entirely. + * Only valid for `soeOPTIONAL` fields; called by the proxy system + * when assigning a `soeDEFAULT` field its zero value. + * + * @param field The field to make absent. + */ void makeFieldAbsent(SField const& field); + + /** Remove the field identified by `field` unconditionally. + * + * @param field The field to remove. + * @return `true` if the field was found and removed. + */ bool delField(SField const& field); + + /** Remove the field at storage index `index` unconditionally. */ void delField(int index); + /** Return the `SOEStyle` (`soeREQUIRED`, `soeOPTIONAL`, `soeDEFAULT`) + * for `field` according to the associated template. + * + * @param field The field to query. + * @return The style, or `SoeInvalid` if the object is in free mode. + */ [[nodiscard]] SOEStyle getStyle(SField const& field) const; + /** Return `true` when any stored field compares equal to `entry`. + * + * Used to test membership in collections such as `STArray`. + */ [[nodiscard]] bool hasMatchingEntry(STBase const&) const; + /** Compare two `STObject` instances for equality. + * + * Only wire-representable (`isBinary()`) fields participate. + * This comparison is O(n²) by design. + * + * @note For fast same-template comparison, use `isEquivalent()` which + * short-circuits when both objects share the same `mType` pointer. + */ bool operator==(STObject const& o) const; bool operator!=(STObject const& o) const; + /** Exception thrown by the proxy and `at()` accessors on field errors. + * + * Raised when a required field is absent, a template constraint is + * violated, or an invalid field access is attempted on a free-mode object. + */ class FieldErr; private: + /** Selects which fields are included during serialization. + * + * The underlying `bool` values alias directly to `SField::shouldInclude(bool)` + * so they can be passed without translation: + * - `OmitSigningFields` (false) — exclude fields not intended for signing. + * - `WithAllFields` (true) — include every field. + */ enum class WhichFields : bool { // These values are carefully chosen to do the right thing if passed // to SField::shouldInclude (bool) @@ -474,15 +822,34 @@ private: //------------------------------------------------------------------------------ +/** Common base for `ValueProxy` and `OptionalProxy`. + * + * Stores a back-pointer to the owning `STObject`, the `SOEStyle` of the + * field, and a typed descriptor pointer. Provides the read path + * (`value()`, `operator*`, `operator->`) and the write primitive `assign()`. + * + * `assign()` enforces `soeDEFAULT` canonicalization: assigning the zero + * value calls `makeFieldAbsent` rather than storing an explicit default, + * preserving canonical wire format. + * + * @tparam T The concrete `STBase`-derived type carrying the field's value. + */ template class STObject::Proxy { public: using value_type = typename T::value_type; + /** Return the field's current value. + * + * For `soeDEFAULT` fields that are absent, returns a default-constructed + * `value_type`. Throws `FieldErr` for absent `soeOPTIONAL` or required + * fields, and when called on a free-mode object with no template. + */ [[nodiscard]] value_type value() const; + /** Dereference operator; equivalent to `value()`. */ value_type operator*() const; @@ -500,36 +867,65 @@ protected: Proxy(STObject* st, TypedField const* f); + /** Locate the field via `dynamic_cast`; returns `nullptr` when absent. */ [[nodiscard]] T const* find() const; + /** Write `u` into the field, applying `soeDEFAULT` canonicalization. */ template void assign(U&& u); }; -// Constraint += and -= ValueProxy operators -// to value types that support arithmetic operations +/** Satisfied by scalar arithmetic types, `Number`, and `STAmount`. + * + * Used to gate `ValueProxy::operator+=` and `operator-=` so they are only + * available for field types that support arithmetic operations. + */ template concept IsArithmeticNumber = std::is_arithmetic_v || std::is_same_v || std::is_same_v; + +/** Satisfied by phantom-typed `unit::ValueUnit` wrappers + * whose `Value` satisfies `IsArithmeticNumber`. + */ template < typename U, typename Value = typename U::value_type, typename Unit = typename U::unit_type> concept IsArithmeticValueUnit = std::is_same_v> && IsArithmeticNumber && std::is_class_v; + +/** Satisfied by ST wrapper types (e.g., `STAmount`) that are not + * `ValueUnit` but whose `value_type` satisfies `IsArithmeticNumber`. + */ template concept IsArithmeticST = !IsArithmeticValueUnit && IsArithmeticNumber; + +/** Union of `IsArithmeticNumber`, `IsArithmeticST`, and `IsArithmeticValueUnit`. */ template concept IsArithmetic = IsArithmeticNumber || IsArithmeticST || IsArithmeticValueUnit; +/** Satisfied when `T + U` compiles and the result is assignable back to `T`. */ template concept Addable = requires(T t, U u) { t = t + u; }; + +/** Satisfied when `T`'s `value_type` is arithmetic and supports addition with `U`. */ template concept IsArithmeticCompatible = IsArithmetic && Addable; +/** Mutable proxy for a non-optional (`soeREQUIRED` or `soeDEFAULT`) field. + * + * Returned by the mutable overloads of `STObject::operator[]` and + * `STObject::at()`. Supports assignment, arithmetic `+=`/`-=` for + * compatible value types, and implicit conversion to `value_type` for + * transparent read-through. + * + * Copy-constructible but not copy-assignable; constructed only by `STObject`. + * + * @tparam T The concrete `STBase`-derived type carrying the field's value. + */ template class STObject::ValueProxy : public Proxy { @@ -541,22 +937,24 @@ public: ValueProxy& operator=(ValueProxy const&) = delete; + /** Assign `u` to the field, delegating to `Proxy::assign()`. */ template std::enable_if_t, ValueProxy&> operator=(U&& u); - // Convenience operators for value types supporting - // arithmetic operations + /** Add `u` to the current field value and write back. */ template requires IsArithmeticCompatible ValueProxy& operator+=(U const& u); + /** Subtract `u` from the current field value and write back. */ template requires IsArithmeticCompatible ValueProxy& operator-=(U const& u); + /** Implicit conversion to `value_type` for transparent read-through. */ operator value_type() const; template @@ -572,6 +970,22 @@ private: ValueProxy(STObject* st, TypedField const* f); }; +/** Mutable proxy for an optional (`soeOPTIONAL`) field. + * + * Returned by the mutable overloads of `STObject::operator[]` and + * `STObject::at()` when called with an `OptionaledField`. Supports + * assignment from a value, `std::optional`, or `std::nullopt` (to remove + * the field). Implicit conversion to `optional_type` enables use in + * standard optional contexts. + * + * Assigning `std::nullopt` to a `soeREQUIRED` or `soeDEFAULT` field throws + * `FieldErr`; required fields cannot be removed and default-value fields are + * semantically always present. + * + * Copy-constructible but not copy-assignable; constructed only by `STObject`. + * + * @tparam T The concrete `STBase`-derived type carrying the field's value. + */ template class STObject::OptionalProxy : public Proxy { @@ -593,6 +1007,7 @@ public: explicit operator bool() const noexcept; + /** Implicit conversion to `std::optional`. */ operator optional_type() const; /** Explicit conversion to std::optional */ @@ -665,17 +1080,26 @@ public: return !(lhs == rhs); } - // Emulate std::optional::value_or + /** Return the field's value if present, otherwise `val`. */ [[nodiscard]] value_type valueOr(value_type val) const; + /** Remove the field (make it absent). + * + * @throws FieldErr if the field is `soeREQUIRED` or `soeDEFAULT`. + */ OptionalProxy& operator=(std::nullopt_t const&); + + /** Assign from an rvalue `std::optional`; removes the field if `nullopt`. */ OptionalProxy& operator=(optional_type&& v); // NOLINT(cppcoreguidelines-rvalue-reference-param-not-moved) + + /** Assign from a const `std::optional`; removes the field if `nullopt`. */ OptionalProxy& operator=(optional_type const& v); + /** Assign a value directly to the field. */ template std::enable_if_t, OptionalProxy&> operator=(U&& u); @@ -685,12 +1109,15 @@ private: OptionalProxy(STObject* st, TypedField const* f); + /** Return `true` when the field is present (not the `STI_NOTPRESENT` sentinel). */ [[nodiscard]] bool engaged() const noexcept; + /** Remove the field, enforcing `soeREQUIRED`/`soeDEFAULT` constraints. */ void disengage(); + /** Return the current value as `optional_type`, or `nullopt` if absent. */ [[nodiscard]] optional_type optionalValue() const; }; diff --git a/include/xrpl/protocol/STParsedJSON.h b/include/xrpl/protocol/STParsedJSON.h index 04ffc624fb..ff4647bdae 100644 --- a/include/xrpl/protocol/STParsedJSON.h +++ b/include/xrpl/protocol/STParsedJSON.h @@ -13,19 +13,51 @@ inline constexpr std::size_t kMAX_PARSED_JSON_DEPTH = 64; Requests exceeding this limit are rejected with an invalidParams error. */ inline constexpr std::size_t kMAX_PARSED_JSON_ARRAY_SIZE = 512; -/** Holds the serialized result of parsing an input JSON object. - This does validation and checking on the provided JSON. -*/ +/** Single-use converter from a JSON object to an @ref STObject. + * + * Sits at the boundary between the JSON/RPC layer and the binary-canonical + * Serialized Type (ST) system. In a single constructor call it validates + * every field name against the XRPL protocol schema, coerces each value to + * its wire type, recurses into nested objects and arrays up to + * @ref kMAX_PARSED_JSON_DEPTH levels deep with each array bounded by + * @ref kMAX_PARSED_JSON_ARRAY_SIZE elements, and applies the field template + * for the transaction or ledger-entry type discovered during parsing. + * + * Outcomes are communicated through two public members rather than via + * exceptions or a return value, which lets RPC handlers forward `error` + * directly to the client without additional formatting work: + * - On success: `object` holds the populated `STObject`; `error` is empty. + * - On failure: `object` is `std::nullopt`; `error` is an + * `rpcINVALID_PARAMS` JSON value with a dot-separated field path + * (e.g. `"tx_json.Signers[0].Signer.Account"`) pinpointing the offending + * field. + * + * The class is non-copyable and not default-constructible; every instance + * represents exactly one completed parse attempt. + * + * @see TransactionSign.cpp for the primary production call-site. + */ class STParsedJSONObject { public: - /** Parses and creates an STParsedJSON object. - The result of the parsing is stored in object and error. - Exceptions: - Does not throw. - @param name The name of the JSON field, used in diagnostics. - @param json The JSON-RPC to parse. - */ + /** Parse @p json into a strongly-typed @ref STObject. + * + * Iterates every member of the JSON object, resolves each field name + * via `SField::getField()`, recurses into nested objects and arrays, + * and finally calls `applyTemplateFromSField()` to enforce the field + * template for the detected transaction or ledger-entry type. + * + * All internal exceptions are caught and translated into a structured + * `rpcINVALID_PARAMS` error stored in `error`; nothing propagates to + * the caller. + * + * @param name The logical name of the top-level field being parsed + * (e.g. `"tx_json"`); used as the root of dot-separated field-path + * strings in `error` messages. + * @param json The JSON object to parse. Must be a JSON object value; + * non-object input produces an `rpcINVALID_PARAMS` error. + * @note Does not throw. + */ STParsedJSONObject(std::string const& name, json::Value const& json); STParsedJSONObject() = delete; @@ -34,10 +66,15 @@ public: operator=(STParsedJSONObject const&) = delete; ~STParsedJSONObject() = default; - /** The STObject if the parse was successful. */ + /** The parsed object on success, or `std::nullopt` on any parse error. */ std::optional object; - /** On failure, an appropriate set of error values. */ + /** Structured `rpcINVALID_PARAMS` error on failure; empty on success. + * + * The JSON value is suitable for forwarding directly to an RPC client. + * The `"error_message"` field contains a dot-separated field path + * identifying the first field that failed to parse. + */ json::Value error; }; diff --git a/include/xrpl/protocol/STPathSet.h b/include/xrpl/protocol/STPathSet.h index a1891164f6..b0b6c6d092 100644 --- a/include/xrpl/protocol/STPathSet.h +++ b/include/xrpl/protocol/STPathSet.h @@ -1,3 +1,14 @@ +/** @file + * Defines the three-class hierarchy for payment path representation in XRPL + * transactions. + * + * `STPathElement` is a single hop; `STPath` is an ordered sequence of hops; + * `STPathSet` is the collection of alternate candidate paths carried in the + * `Paths` field of a `Payment` transaction on the wire. Together they encode + * how a cross-currency payment routes through the order book and the trust-line + * graph from source to destination. + */ + #pragma once #include @@ -14,6 +25,24 @@ namespace xrpl { +/** A single hop in a payment path. + * + * A node is either an *account node* (rippling through trust lines) or an + * *offer node* (matching against a DEX order book). `isOffer()` returns `true` + * when `mAccountID` is the XRP "no account" sentinel; otherwise the element + * represents an account. The `Type` bitmask drives both on-wire encoding and + * runtime dispatch. + * + * The asset field holds a `PathAsset` variant (`Currency` for legacy IOU hops, + * `MPTID` for MPT hops). `TypeCurrency` and `TypeMpt` are mutually exclusive. + * + * A non-cryptographic hash of the account, asset, and issuer fields is + * pre-computed at construction (`hash_value_`). `operator==` short-circuits on + * this hash before performing field-by-field comparison, making duplicate + * detection in the pathfinder fast over the small vectors used in practice. + * + * @see STPath, STPathSet + */ class STPathElement final : public CountedObject { unsigned int type_; @@ -25,97 +54,217 @@ class STPathElement final : public CountedObject std::size_t hash_value_; public: - // Bitwise values (typeCurrency | typeMPT) + /** Bitmask constants that govern on-wire encoding and runtime dispatch. + * + * Each hop's type byte is the OR of the applicable constants. The + * deserializer rejects any byte with bits outside `TypeAll` as malformed. + * `TypeCurrency` and `TypeMpt` are mutually exclusive; `TypeAsset` is a + * convenience mask to test either without caring which. + */ // NOLINTNEXTLINE(cppcoreguidelines-use-enum-class) enum Type { - TypeNone = 0x00, - TypeAccount = 0x01, // Rippling through an account (vs taking an offer). - TypeCurrency = 0x10, // Currency follows. - TypeIssuer = 0x20, // Issuer follows. - TypeMpt = 0x40, // MPT follows. - TypeBoundary = 0xFF, // Boundary between alternate paths. - TypeAsset = TypeCurrency | TypeMpt, - TypeAll = TypeAccount | TypeCurrency | TypeIssuer | TypeMpt, - // Combination of all types. + TypeNone = 0x00, /**< Path terminator (0x00 byte ends the PathSet). */ + TypeAccount = 0x01, /**< Account field is present; node ripples through trust lines. */ + TypeCurrency = 0x10, /**< Legacy IOU Currency (160-bit) follows. Mutually exclusive with TypeMpt. */ + TypeIssuer = 0x20, /**< Issuer AccountID (160-bit) follows. */ + TypeMpt = 0x40, /**< MPT issuance ID (192-bit MPTID) follows. Mutually exclusive with TypeCurrency. */ + TypeBoundary = 0xFF, /**< Separator between consecutive paths within the PathSet. */ + TypeAsset = TypeCurrency | TypeMpt, /**< Either asset kind; tests presence without distinguishing IOU vs MPT. */ + TypeAll = TypeAccount | TypeCurrency | TypeIssuer | TypeMpt, /**< Union of all valid type bits; used to validate incoming bytes. */ }; + /** Construct a `TypeNone` (path-terminator / empty) element. + * + * The resulting element has `is_offer_ = true` and all fields zero. + * Used as a sentinel and by default-constructed STPath entries. + */ STPathElement(); STPathElement(STPathElement const&) = default; STPathElement& operator=(STPathElement const&) = default; + /** Construct an element from optional fields, setting type bits automatically. + * + * The type bitmask is derived from which optionals are non-null: + * `TypeAccount` if `account` is set, `TypeCurrency`/`TypeMpt` from the + * `PathAsset` variant if `asset` is set, and `TypeIssuer` if `issuer` is + * set. Asserts (debug builds) that account and issuer are not `noAccount()` + * when provided. + * + * @param account AccountID of the hop; absent means this is an offer node. + * @param asset PathAsset (Currency or MPTID) for the hop; absent means + * no asset constraint. + * @param issuer Issuer AccountID; absent means no issuer constraint. + */ STPathElement( std::optional const& account, std::optional const& asset, std::optional const& issuer); + /** Construct an element from explicit non-optional fields. + * + * `TypeAccount` is set when `account` is not the XRP sentinel; the asset + * type bit (`TypeCurrency` or `TypeMpt`) is set when the asset is not XRP + * (or unconditionally when `forceAsset` is `true`); `TypeIssuer` is set + * when `issuer` is not the XRP sentinel. + * + * @param account AccountID of the hop; XRP sentinel (`xrpAccount()`) + * means offer node. + * @param asset PathAsset describing the hop's currency or MPT. + * @param issuer Issuer AccountID. + * @param forceAsset When `true`, always set the asset type bit even if the + * asset is XRP. Used to preserve currency information in offer nodes + * whose asset happens to be XRP. + */ STPathElement( AccountID const& account, PathAsset const& asset, AccountID const& issuer, bool forceAsset = false); + /** Construct an element from an explicit wire-format type byte and fields. + * + * Used by the deserializer. The type byte is accepted verbatim and then + * sanitised: the actual `PathAsset` variant is inspected to clear the + * contradictory bit (`TypeMpt` when holding a `Currency`, or + * `TypeCurrency` when holding an `MPTID`), so a caller cannot pass a + * self-contradictory bitmask. + * + * @param uType Wire-format type bitmask from the stream. + * @param account AccountID of the hop. + * @param asset PathAsset (Currency or MPTID) for the hop. + * @param issuer Issuer AccountID. + */ STPathElement( unsigned int uType, AccountID const& account, PathAsset const& asset, AccountID const& issuer); + /** Return the raw type bitmask for this element. + * + * The result is the OR of the applicable `Type` constants and can be + * inspected with `isType()` or tested directly against the `Type` enum. + */ [[nodiscard]] auto getNodeType() const; + /** Return `true` if this element represents a DEX offer node. + * + * An element is an offer node when its account field is the XRP "no + * account" sentinel, meaning the hop matches against the order book + * rather than rippling through a trust line. + */ [[nodiscard]] bool isOffer() const; + /** Return `true` if this element represents a trust-line account node. + * + * Equivalent to `!isOffer()`. + */ [[nodiscard]] bool isAccount() const; + /** Return `true` if the `TypeIssuer` bit is set. */ [[nodiscard]] bool hasIssuer() const; + /** Return `true` if the `TypeCurrency` bit is set (legacy IOU hop). */ [[nodiscard]] bool hasCurrency() const; + /** Return `true` if the `TypeMpt` bit is set (MPT hop). */ [[nodiscard]] bool hasMPT() const; + /** Return `true` if any asset type bit (`TypeCurrency` or `TypeMpt`) is set. */ [[nodiscard]] bool hasAsset() const; + /** Return `true` if this element is a path terminator (`TypeNone`). */ [[nodiscard]] bool isNone() const; - // Nodes are either an account ID or a offer prefix. Offer prefixs denote a - // class of offers. + /** Return the account for this hop. + * + * For account nodes this is the AccountID through which the payment + * ripples. For offer nodes the field holds the XRP "no account" sentinel + * and callers should use `isOffer()` to distinguish the two cases before + * interpreting this value. + */ [[nodiscard]] AccountID const& getAccountID() const; + /** Return the `PathAsset` (Currency or MPTID) for this hop. */ [[nodiscard]] PathAsset const& getPathAsset() const; + /** Return the `Currency` for this hop. + * + * @note Only valid when `hasCurrency()` is `true`; the underlying + * `PathAsset::get()` throws on type mismatch in debug builds. + */ [[nodiscard]] Currency const& getCurrency() const; + /** Return the `MPTID` for this hop. + * + * @note Only valid when `hasMPT()` is `true`; the underlying + * `PathAsset::get()` throws on type mismatch in debug builds. + */ [[nodiscard]] MPTID const& getMPTID() const; + /** Return the issuer AccountID for this hop. */ [[nodiscard]] AccountID const& getIssuerID() const; + /** Return `true` if any bit of `pe` is set in the element's type bitmask. + * + * @param pe Type mask to test; typically a single `Type` constant or a + * bitwise OR of several. + */ [[nodiscard]] bool isType(Type const& pe) const; + /** Return `true` if the two elements have identical account, asset, and issuer fields. + * + * Short-circuits first on the `TypeAccount` bit and the pre-computed hash + * before performing full field comparison, making deduplication fast over + * the small path vectors used in practice. + */ bool operator==(STPathElement const& t) const; + /** Return `true` if the two elements differ in any field. */ bool operator!=(STPathElement const& t) const; private: + /** Compute the non-cryptographic hash stored in `hash_value_`. + * + * Uses FNV-style multiply-XOR with distinct primes (257, 509, 911) for + * the account, asset, and issuer fields respectively, then XORs the three + * sub-hashes together. Reads the actual `PathAsset` variant via `visit()` + * rather than the type bitmask, because the bitmask may be partially set + * during pathfinder construction. Speed dominates; cryptographic strength + * is not required. + * + * @param element The element to hash. + * @return Non-cryptographic hash combining account, asset, and issuer. + */ static std::size_t getHash(STPathElement const& element); }; +/** An ordered sequence of `STPathElement` hops describing one candidate payment path. + * + * Wraps a `std::vector` with a standard container interface. + * The XRPL protocol caps path length, so the underlying vector is short in + * practice (typically 2–6 elements); linear scans are therefore acceptable. + * + * @see STPathElement, STPathSet + */ class STPath final : public CountedObject { std::vector path_; @@ -123,54 +272,111 @@ class STPath final : public CountedObject public: STPath() = default; + /** Construct a path from an existing vector of elements. + * + * @param p Elements to populate the path; moved into internal storage. + */ STPath(std::vector p); + /** Return the number of hops in this path. */ [[nodiscard]] std::vector::size_type size() const; + /** Return `true` if the path contains no hops. */ [[nodiscard]] bool empty() const; + /** Append a copy of `e` to the end of this path. */ void pushBack(STPathElement const& e); + /** Emplace a new element at the end of this path. + * + * @tparam Args Argument types forwarded to `STPathElement`'s constructor. + * @param args Arguments forwarded to the new element's constructor. + */ template void emplaceBack(Args&&... args); + /** Return `true` if any hop in this path matches the given (account, asset, issuer) triple. + * + * Used by the pathfinder for cycle detection: before extending a path, + * `Pathfinder::addLink()` calls this to ensure the candidate hop has not + * already appeared earlier in the path. A linear scan is acceptable + * because XRPL path lengths are protocol-bounded. + * + * @param account AccountID of the hop to search for. + * @param asset PathAsset (Currency or MPTID) to match. + * @param issuer Issuer AccountID to match. + * @return `true` if any existing element equals all three arguments. + */ [[nodiscard]] bool hasSeen(AccountID const& account, PathAsset const& asset, AccountID const& issuer) const; + /** Serialize the path to a JSON array of hop objects. + * + * Each hop object always includes a `type` field. Optional `account`, + * `currency`, `mpt_issuance_id`, and `issuer` keys are present only when + * the corresponding type bit is set. + * + * @return JSON array where each element describes one hop. + */ [[nodiscard]] json::Value getJson(JsonOptions) const; + /** Return an iterator to the first element. */ [[nodiscard]] std::vector::const_iterator begin() const; + /** Return a past-the-end iterator. */ [[nodiscard]] std::vector::const_iterator end() const; + /** Return `true` if both paths contain identical elements in identical order. */ bool operator==(STPath const& t) const; + /** Return a reference to the last element. */ [[nodiscard]] std::vector::const_reference back() const; + /** Return a reference to the first element. */ [[nodiscard]] std::vector::const_reference front() const; + /** Return a mutable reference to the element at index `i`. */ STPathElement& operator[](int i); + /** Return a const reference to the element at index `i`. */ STPathElement const& operator[](int i) const; + /** Reserve capacity for `s` elements, avoiding reallocations during path construction. + * + * @param s Minimum capacity to reserve. + */ void reserve(size_t s); }; //------------------------------------------------------------------------------ -// A set of zero or more payment paths +/** The serialized `Paths` field of a Payment transaction — a collection of alternate payment paths. + * + * Inherits from `STBase` and participates in the ledger type system via + * `STI_PATHSET`. The binary wire format encodes each `STPath` as a sequence + * of type-tagged hop records; consecutive paths are delimited by + * `TypeBoundary` (0xFF) and the entire field is terminated by `TypeNone` + * (0x00). Deserialization throws `std::runtime_error` on malformed input + * (empty paths or unknown type bits). + * + * The `isDefault()` override returns `true` when the set is empty, allowing + * the serialization layer to elide the field from transactions that have no + * explicit paths. + * + * @see STPath, STPathElement + */ class STPathSet final : public STBase, public CountedObject { std::vector value_; @@ -178,55 +384,140 @@ class STPathSet final : public STBase, public CountedObject public: STPathSet() = default; + /** Construct an empty STPathSet named by `n`. */ STPathSet(SField const& n); + + /** Deserialize an STPathSet from a binary stream. + * + * Reads the wire format produced by `add()`: hop records delimited by + * `TypeBoundary` (0xFF) and terminated by `TypeNone` (0x00). + * + * @param sit Binary cursor positioned at the first type byte; advanced + * past the terminating `TypeNone` on return. + * @param name SField that names this field in the enclosing object. + * @throws std::runtime_error "empty path" if a boundary or terminator is + * encountered before any hop is accumulated. + * @throws std::runtime_error "bad path element" if a type byte contains + * bits outside `TypeAll`. + */ STPathSet(SerialIter& sit, SField const& name); + /** Serialize the path set to its canonical binary wire format. + * + * Emits each hop as a type byte followed by its optional account (20B), + * MPTID (24B), currency (20B), and/or issuer (20B) payloads. Consecutive + * paths are separated by `TypeBoundary` (0xFF); the set ends with + * `TypeNone` (0x00). + * + * @param s Serializer accumulator to which bytes are appended. + */ void add(Serializer& s) const override; - [[nodiscard]] json::Value getJson(JsonOptions) const override; + /** Serialize the path set to a JSON array of path arrays. + * + * @param options JSON rendering options forwarded to each path. + * @return Nested JSON array: `[[hop, ...], [hop, ...], ...]`. + */ + [[nodiscard]] json::Value getJson(JsonOptions options) const override; + /** Return `STI_PATHSET`, identifying this field to the serialization framework. */ [[nodiscard]] SerializedTypeID getSType() const override; + /** Append `base` extended by `tail` to the set, unless an identical path already exists. + * + * Used by the pathfinder to build candidate paths incrementally. The + * candidate is pushed onto `value_` and then scanned against existing + * paths in reverse order (newest-first) to detect duplicates; if found, + * the candidate is popped and `false` is returned. Reverse iteration is a + * micro-optimisation because duplicates are most likely among recently + * added paths. + * + * @param base Prefix path to extend. + * @param tail Single hop appended to `base` before comparison. + * @return `true` if the path was added; `false` if it was a duplicate. + */ bool assembleAdd(STPath const& base, STPathElement const& tail); + /** Return `true` if `t` is an STPathSet with identical path contents. + * + * @param t Object to compare; returns `false` immediately if not an STPathSet. + */ [[nodiscard]] bool isEquivalent(STBase const& t) const override; + /** Return `true` when the set contains no paths. + * + * The serialization layer uses this to elide the `Paths` field from + * transactions that require no explicit pathfinding routes. + */ [[nodiscard]] bool isDefault() const override; - // std::vector like interface: + // std::vector-like interface for iterating and indexing paths. + + /** Return a const reference to the path at index `n`. */ std::vector::const_reference operator[](std::vector::size_type n) const; + /** Return a mutable reference to the path at index `n`. */ std::vector::reference operator[](std::vector::size_type n); + /** Return an iterator to the first path. */ [[nodiscard]] std::vector::const_iterator begin() const; + /** Return a past-the-end iterator. */ [[nodiscard]] std::vector::const_iterator end() const; + /** Return the number of paths in the set. */ [[nodiscard]] std::vector::size_type size() const; + /** Return `true` if the set contains no paths. */ [[nodiscard]] bool empty() const; + /** Append a copy of `e` to the set. + * + * @note Does not deduplicate; use `assembleAdd()` when deduplication is needed. + */ void pushBack(STPath const& e); + /** Emplace a new path at the end of the set. + * + * @tparam Args Argument types forwarded to `STPath`'s constructor. + * @param args Arguments forwarded to the new path's constructor. + */ template void emplaceBack(Args&&... args); private: + /** Copy-construct this STPathSet into `buf` via placement-new. + * + * Plugs STPathSet into the `detail::STVar` small-object storage scheme. + * + * @param n Byte size of `buf`; must be at least `sizeof(STPathSet)`. + * @param buf Aligned destination buffer. + * @return Pointer to the newly constructed object. + */ STBase* copy(std::size_t n, void* buf) const override; + + /** Move-construct this STPathSet into `buf` via placement-new. + * + * Plugs STPathSet into the `detail::STVar` small-object storage scheme. + * + * @param n Byte size of `buf`; must be at least `sizeof(STPathSet)`. + * @param buf Aligned destination buffer. + * @return Pointer to the newly constructed object. + */ STBase* move(std::size_t n, void* buf) override; @@ -369,8 +660,6 @@ STPathElement::isNone() const return getNodeType() == STPathElement::TypeNone; } -// Nodes are either an account ID or a offer prefix. Offer prefixs denote a -// class of offers. inline AccountID const& STPathElement::getAccountID() const { @@ -499,7 +788,6 @@ inline STPathSet::STPathSet(SField const& n) : STBase(n) { } -// std::vector like interface: inline std::vector::const_reference STPathSet::operator[](std::vector::size_type n) const { diff --git a/include/xrpl/protocol/STTakesAsset.h b/include/xrpl/protocol/STTakesAsset.h index bf75ffccf7..a86601ee95 100644 --- a/include/xrpl/protocol/STTakesAsset.h +++ b/include/xrpl/protocol/STTakesAsset.h @@ -5,28 +5,48 @@ namespace xrpl { -/** Intermediate class for any STBase-derived class to store an Asset. +/** Mixin base that lets a serializable field receive an `Asset` at runtime. * - * In the class definition, this class should be specified as a base class - * _instead_ of STBase. + * Derived classes inherit from `STTakesAsset` _instead of_ `STBase` when + * they store a numeric quantity whose precision depends on the enclosing + * ledger entry's asset type (XRP, IOU, or MPT). The asset identity is + * already present in the containing ledger object and must not be duplicated + * in each field; `STTakesAsset` carries it at runtime without serializing it. * - * Specifically, the Asset is only stored and used at runtime. It should not be - * serialized to the ledger. + * The only current concrete user is `STNumber`, which overrides + * `associateAsset()` to round its stored `Number` to the asset's canonical + * precision immediately upon association and again during serialization. * - * The derived class decides what to do with the Asset, and when. It will not - * necessarily be set at any given time. As of this writing, only STNumber uses - * it to round the stored Number to the Asset's precision both when associated, - * and when serializing the Number. + * @note `asset_` is intentionally `std::optional`: during deserialization from + * disk no transactor context is available, so no asset can be supplied. + * The value still round-trips correctly because it was already rounded when + * originally written. + * @see STNumber, associateAsset(STLedgerEntry&, Asset const&) */ class STTakesAsset : public STBase { protected: + /** Runtime asset identity used for precision rounding. + * + * Absent (`std::nullopt`) on the deserialization path; set by a call to + * `associateAsset()` inside `doApply()` before the SLE is serialized. + */ std::optional asset_; public: using STBase::STBase; using STBase::operator=; + /** Record @p a as the asset governing this field's precision. + * + * The base implementation stores @p a in `asset_` via `emplace` and + * returns. Derived classes override this method to act on the asset + * immediately — for example, `STNumber` also rounds its stored value to + * the asset's canonical precision. + * + * @param a The asset to associate. Must be the same asset used by all + * other `sMD_NeedsAsset`-flagged fields in the enclosing SLE. + */ virtual void associateAsset(Asset const& a); }; @@ -39,20 +59,26 @@ STTakesAsset::associateAsset(Asset const& a) class STLedgerEntry; -/** Associate an Asset with all sMD_NeedsAsset fields in a ledger entry. +/** Associate an asset with every `sMD_NeedsAsset`-flagged field in @p sle. * - * This function iterates over all fields in the given ledger entry. For each - * field that is set and has the SField::sMD_NeedsAsset metadata flag, it calls - * `associateAsset` on that field with the given Asset. Such field must be - * derived from STTakesAsset - if it is not, the conversion will throw. + * Iterates over all fields in @p sle by offset (the only path that yields + * mutable `STBase&` references). For each field that is present and carries + * `SField::kSMD_NEEDS_ASSET`, calls `associateAsset(asset)` on it, triggering + * derived-class rounding logic (e.g., `STNumber` rounds to the asset's + * canonical precision). After rounding, any `soeDEFAULT`-style field whose + * value has become the default (e.g., rounded down to zero) is removed from + * the SLE via `makeFieldAbsent` so that zero defaults are not persisted in the + * ledger. * - * Typically, associateAsset should be called near the end of doApply() of any - * Transactor classes on the SLEs of any new or modified ledger entries - * containing STNumber fields, after doing all of the modifications t the SLEs. - * - * @param sle The ledger entry whose fields will be updated. - * @param asset The Asset to associate with the relevant fields. + * Call this near the end of `doApply()` in any transactor that creates or + * modifies an SLE containing `STNumber` fields, after all other mutations to + * the SLE are complete. Rounding before computations finish may distort + * intermediate values. * + * @param sle The ledger entry whose `sMD_NeedsAsset` fields will be updated. + * @param asset The asset that governs precision for all such fields in @p sle. + * @throws std::bad_cast if any field carrying `kSMD_NEEDS_ASSET` is not + * derived from `STTakesAsset` — this indicates a field schema error. */ void associateAsset(STLedgerEntry& sle, Asset const& asset); diff --git a/include/xrpl/protocol/STTx.h b/include/xrpl/protocol/STTx.h index d1bd32848f..3e16a447af 100644 --- a/include/xrpl/protocol/STTx.h +++ b/include/xrpl/protocol/STTx.h @@ -1,3 +1,8 @@ +/** @file + * Declares `STTx`, the canonical in-memory representation of an XRP Ledger + * transaction, together with the free functions that operate on it + * (`passesLocalChecks`, `sterilize`, `isPseudoTx`). + */ #pragma once #include @@ -15,67 +20,171 @@ namespace xrpl { +/** Status codes used to tag a transaction row in the local SQLite + * `Transactions` table. + * + * Each enumerator maps to the single-character `Status` column value + * stored by `getMetaSQL`. + */ enum class TxnSql : char { - New = 'N', - Conflict = 'C', - Held = 'H', - Validated = 'V', - Included = 'I', - Unknown = 'U' + New = 'N', /**< Transaction has just been received and not yet processed. */ + Conflict = 'C', /**< Transaction conflicts with a previously applied transaction. */ + Held = 'H', /**< Transaction is queued but not yet eligible for inclusion. */ + Validated = 'V', /**< Transaction is in a validated ledger. */ + Included = 'I', /**< Transaction is included in a pending ledger. */ + Unknown = 'U' /**< Transaction status cannot be determined. */ }; +/** The canonical in-memory representation of an XRP Ledger transaction. + * + * `STTx` extends `STObject` with transaction-specific identity, typing, + * signing, and persistence semantics. It caches the transaction ID (`tid_`, + * a SHA-512 half-hash prefixed with `HashPrefix::transactionID`) and the + * decoded transaction type (`tx_type_`) so that hot paths avoid repeated + * field lookups and hash recomputation. + * + * Three construction paths exist: wire deserialization (`SerialIter&`), + * object promotion (`STObject&&`), and programmatic assembly + * (`TxType, assembler`). Copy construction is allowed; copy assignment is + * deleted to prevent invariant violations on re-assignment. + * + * The class is `final`: transaction-type-specific behavior lives in the + * transactor subsystem, not in subclasses of `STTx`. + * + * @note `CountedObject` tracks live instance counts for diagnostics. + */ class STTx final : public STObject, public CountedObject { uint256 tid_; TxType tx_type_; public: + /** Minimum number of signers allowed in a multi-sign signer list. */ static constexpr std::size_t kMIN_MULTI_SIGNERS = 1; + /** Maximum number of signers allowed in a multi-sign signer list. */ static constexpr std::size_t kMAX_MULTI_SIGNERS = 32; STTx() = delete; STTx(STTx const& other) = default; + /** Deleted to prevent re-assignment from invalidating the cached ID and type. */ STTx& operator=(STTx const& other) = delete; + /** Deserialize a transaction from a wire-format byte stream. + * + * Validates the remaining byte count against the + * `kTX_MIN_SIZE_BYTES`/`kTX_MAX_SIZE_BYTES` protocol bounds, parses + * the field stream, applies the `SOTemplate` for the decoded + * `TxType`, and caches the transaction ID. This is the hottest + * construction path: every inbound peer transaction and every + * transaction loaded from the node store passes through here. + * + * @param sit A `SerialIter` positioned at the first byte of the + * serialized transaction. The iterator is advanced in place. + * @throws std::runtime_error if the byte count is outside protocol + * bounds, if an object-terminator byte is encountered at the top + * level, if the transaction type is unregistered, or if + * `applyTemplate` rejects the field layout. + */ explicit STTx(SerialIter& sit); + + /** Rvalue-reference overload that delegates to the lvalue constructor. + * + * `SerialIter` is consumed by value semantics internally, so the + * rvalue is not actually moved; the `// NOLINT` in the inline + * definition acknowledges this. + * + * @param sit A temporary `SerialIter`; forwarded to `STTx(SerialIter&)`. + * @throws std::runtime_error (same conditions as the lvalue overload). + */ explicit STTx(SerialIter&& sit); + + /** Promote a generic `STObject` to a fully typed transaction. + * + * Used when a transaction arrives as a raw parsed object (e.g., from + * JSON deserialization) and must be graduated to a fully validated + * `STTx`. No wire-size checks are performed; `applyTemplate` enforces + * field conformance against the registered `SOTemplate` for the + * transaction type. + * + * @param object An rvalue `STObject` that must already contain + * `sfTransactionType`. Consumed by the move. + * @throws std::runtime_error if the transaction type is unregistered + * or if `applyTemplate` rejects the field layout. + */ explicit STTx(STObject&& object); - /** Constructs a transaction. - - The returned transaction will have the specified type and - any fields that the callback function adds to the object - that's passed in. - */ + /** Programmatically construct a transaction of the given type. + * + * Installs the `SOTemplate` for `type` and sets `sfTransactionType`, + * then invokes `assembler` to populate remaining fields. After the + * assembler returns, the transaction ID is computed and cached. + * + * @param type The transaction type; must be registered in + * `TxFormats`. + * @param assembler A callable invoked with a mutable reference to the + * newly templated `STObject`. Must not mutate `sfTransactionType`. + * @throws std::runtime_error if `type` is not registered. + * @note Fires `logicError` (not a thrown exception) if `assembler` + * mutates `sfTransactionType` — this is a programming error, not a + * data error. + */ STTx(TxType type, std::function assembler); - // STObject functions. + /** @return The serialized type ID `STI_TRANSACTION`. */ SerializedTypeID getSType() const override; + /** @return A human-readable string of the form `"" = { ... }`. */ std::string getFullText() const override; - // Outer transaction functions / signature functions. + /** Extract the raw `sfTxnSignature` bytes from an arbitrary object. + * + * @param sigObject The object to read `sfTxnSignature` from; typically + * `*this` or a multi-sign signer sub-object. + * @return The signature bytes, or an empty `Blob` if the field is absent + * or an exception occurs during field access. + */ static Blob getSignature(STObject const& sigObject); + /** Extract the `sfTxnSignature` bytes from this transaction. */ Blob getSignature() const { return getSignature(*this); } + /** Compute the single-sign hash of this transaction. + * + * Prepends `HashPrefix::TxSign` to the serialized form (without signing + * fields) and returns the SHA-512 half-hash. Use this to verify a + * signature without calling `checkSign`. + * + * @return The 256-bit signing hash. + */ uint256 getSigningHash() const; + /** @return The decoded transaction type, cached at construction time. */ TxType getTxnType() const; + /** @return The raw bytes of `sfSigningPubKey`. Empty for multi-signed transactions. */ Blob getSigningPubKey() const; + /** Return a unified sequence proxy abstracting classic sequence and ticket modes. + * + * When `sfSequence` is non-zero the transaction uses classic sequence + * ordering and a `SeqProxy::Sequence` is returned. When `sfSequence` is + * zero and `sfTicketSequence` is present, a `SeqProxy::Ticket` is returned. + * Sequence-type proxies always sort before ticket-type proxies, which the + * protocol relies on for correct processing order. + * + * @return A `SeqProxy` of type `Sequence` or `Ticket` as appropriate. + */ SeqProxy getSeqProxy() const; @@ -83,44 +192,150 @@ public: std::uint32_t getSeqValue() const; + /** Resolve the account whose balance pays the transaction fee. + * + * Returns `sfDelegate` if present, otherwise `sfAccount`. Authorization + * of the delegate relationship is enforced separately in the transactor + * layer; this method performs no validation. + * + * @return The `AccountID` of the fee-paying account. + */ AccountID getFeePayer() const; + /** Collect every `AccountID` referenced by top-level fields of this transaction. + * + * Walks top-level `STAccount` fields and non-XRP `STAmount` issuers. + * Used to determine which accounts are touched by a transaction for + * indexing and fee purposes. + * + * @return A flat, sorted set of all referenced account IDs. + * @note Only top-level fields are examined; nested objects (e.g., + * inner multi-sign signers) are not descended into. + */ boost::container::flat_set getMentionedAccounts() const; + /** @return The cached transaction ID, computed at construction time. */ uint256 getTransactionID() const; + /** Return the transaction as a JSON object, optionally including the hash. + * + * Includes the `"hash"` key unless `options` has + * `JsonOptions::DisableApiPriorV2` set (API v2+). + * + * @param options JSON rendering options. + * @return A `json::Value` object representing the transaction. + */ json::Value getJson(JsonOptions options) const override; + /** Return the transaction as JSON, with an optional binary representation. + * + * When `binary` is `true`, the transaction body is hex-encoded. Under + * API v1 the result wraps the hex in `{"tx": "...", "hash": "..."}`; + * under API v2+ it returns the raw hex string. When `binary` is `false`, + * behaves identically to `getJson(options)`. + * + * @param options JSON rendering options controlling API version behavior. + * @param binary If `true`, serialize the transaction to hex instead of + * expanding fields into JSON. + * @return A `json::Value` containing the transaction representation. + */ json::Value getJson(JsonOptions options, bool binary) const; + /** Sign this transaction with the given key pair. + * + * Computes the single-sign payload (hash-prefix + transaction body + * without signing fields), signs it, and writes the signature and public + * key into the transaction. The cached transaction ID is recomputed + * after the signature is stored. + * + * @param publicKey The signer's public key; written to + * `sfSigningPubKey`. + * @param secretKey The corresponding secret key used to produce + * the signature. + * @param signatureTarget If set, the signature is written into that + * named sub-object field (e.g., `sfCounterpartySignature`) instead + * of the transaction root. Used for two-party protocols such as + * `LoanSet`. + */ void sign( PublicKey const& publicKey, SecretKey const& secretKey, std::optional> signatureTarget = {}); - /** Check the signature. - @param rules The current ledger rules. - @return `true` if valid signature. If invalid, the error message string. - */ + /** Verify the primary signature and, if present, the counterparty signature. + * + * Dispatches to single-sign or multi-sign verification based on whether + * `sfSigningPubKey` is empty. If `sfCounterpartySignature` is present, + * it is verified with the same dispatch; errors from the counterparty + * check are prefixed with `"Counterparty: "`. + * + * @param rules The current ledger rules. + * @return An empty `Expected` on success, or an error string on failure. + */ Expected checkSign(Rules const& rules) const; + /** Verify all batch-signing signatures on a `ttBATCH` transaction. + * + * Iterates over `sfBatchSigners`, dispatching each entry to single- or + * multi-sign batch verification. The signed payload is the output of + * `serializeBatch()` — a batch-specific hash prefix, the outer + * transaction's flags, and the IDs of the inner transactions — which + * binds each signer to the exact set of inner transactions. + * + * @param rules The current ledger rules. + * @return An empty `Expected` on success, or an error string on failure. + * @note Asserts and returns an error if called on a non-batch transaction. + */ Expected checkBatchSign(Rules const& rules) const; - // SQL Functions with metadata. + /** Return the static SQL `INSERT OR REPLACE INTO Transactions` header. + * + * The returned string is the constant prefix used by `getMetaSQL` to + * build persistence statements for the local SQLite `Transactions` table. + * + * @return A reference to a process-lifetime static string. + */ static std::string const& getMetaSQLInsertReplaceHeader(); + /** Produce a SQL value tuple for this transaction with `Validated` status. + * + * Serializes the transaction and delegates to the full overload with + * `TxnSql::Validated` as the status code. + * + * @param inLedger The ledger sequence number containing this + * transaction. + * @param escapedMetaData Pre-escaped binary metadata string for the + * `TxnMeta` column. + * @return A SQL value tuple string suitable for appending to + * `getMetaSQLInsertReplaceHeader()`. + */ std::string getMetaSQL(std::uint32_t inLedger, std::string const& escapedMetaData) const; + /** Produce a SQL value tuple with explicit status and raw transaction bytes. + * + * Formats a parenthesized row for the `Transactions` table containing + * the transaction ID, type name, source account (Base58), sequence + * number, ledger sequence, a single-character status code, the raw + * serialized transaction blob, and pre-escaped metadata. + * + * @param rawTxn The serialized transaction bytes (by value). + * @param inLedger The ledger sequence number containing this + * transaction. + * @param status The persistence status code for the `Status` + * column. + * @param escapedMetaData Pre-escaped binary metadata for `TxnMeta`. + * @return A SQL value tuple string. + */ std::string getMetaSQL( Serializer rawTxn, @@ -128,54 +343,118 @@ public: TxnSql status, std::string const& escapedMetaData) const; + /** Return the cached IDs of the inner transactions in a `ttBATCH` transaction. + * + * On the first call, hashes each entry in `sfRawTransactions` and + * stores the result in `batchTxnIds_`. Subsequent calls return the + * cached vector directly. An assertion on every call verifies that the + * cache size still matches `sfRawTransactions`, enforcing the invariant + * that inner transactions may not be modified after the IDs have been + * observed. + * + * @return A const reference to the cached vector of inner transaction IDs. + * @note Must only be called on a `ttBATCH` transaction with a non-empty + * `sfRawTransactions` array. + */ std::vector const& getBatchTransactionIDs() const; private: - /** Check the signature. - @param rules The current ledger rules. - @param sigObject Reference to object that contains the signature fields. - Will be *this more often than not. - @return `true` if valid signature. If invalid, the error message string. - */ + /** Dispatch to single- or multi-sign verification for an arbitrary object. + * + * Inspects `sfSigningPubKey` in `sigObject`: empty → multi-sign path, + * non-empty → single-sign path. + * + * @param rules The current ledger rules. + * @param sigObject The object carrying signature fields; usually `*this` + * but may be a counterparty sub-object. + * @return An empty `Expected` on success, or an error string on failure. + */ Expected checkSign(Rules const& rules, STObject const& sigObject) const; + /** Verify a single-sign signature against the transaction body. */ Expected checkSingleSign(STObject const& sigObject) const; + /** Verify multi-sign signatures against the transaction body. */ Expected checkMultiSign(Rules const& rules, STObject const& sigObject) const; + /** Verify a single-sign batch signature for one `sfBatchSigners` entry. */ Expected checkBatchSingleSign(STObject const& batchSigner) const; + /** Verify multi-sign batch signatures for one `sfBatchSigners` entry. */ Expected checkBatchMultiSign(STObject const& batchSigner, Rules const& rules) const; + /** Placement-new copy into a pre-allocated buffer; supports `STVar` SOO. */ STBase* copy(std::size_t n, void* buf) const override; + /** Placement-new move into a pre-allocated buffer; supports `STVar` SOO. */ STBase* move(std::size_t n, void* buf) override; friend class detail::STVar; + /** Lazily populated cache of inner transaction IDs for `ttBATCH` transactions. */ mutable std::vector batchTxnIds_; }; +/** Run all local pre-submission validity checks on a transaction object. + * + * Gate-keeps local relay and submission by enforcing: + * - Memo field size (max 1024 bytes serialized) and RFC 3986 character + * legality for `MemoType`/`MemoFormat`. + * - All `STAccount` fields must carry non-zero (non-default) values. + * - Pseudo-transaction types (`ttAMENDMENT`, `ttFEE`, `ttUNL_MODIFY`) are + * rejected; they are synthesized internally by the ledger. + * - MPT amounts may only appear in fields that explicitly declare MPT support + * via `soeMPTSupported`. + * - Batch inner transactions must not themselves be `ttBATCH`, and the + * `sfRawTransactions` / `sfBatchSigners` arrays must not exceed + * `kMAX_BATCH_TX_COUNT` entries. + * + * This is a free function rather than an `STTx` method because it can run + * on any `STObject` before it is promoted to a full `STTx`. + * + * @param st The transaction object to validate. + * @param reason Populated with a human-readable failure description when + * the function returns `false`. + * @return `true` if all checks pass; `false` on the first failure. + */ bool -passesLocalChecks(STObject const& st, std::string&); +passesLocalChecks(STObject const& st, std::string& reason); -/** Sterilize a transaction. - - The transaction is serialized and then deserialized, - ensuring that all equivalent transactions are in canonical - form. This also ensures that program metadata such as - the transaction's digest, are all computed. -*/ +/** Canonicalize a transaction via a serialize-then-deserialize round trip. + * + * Serializes `stx` to bytes, then constructs a fresh `STTx` from those bytes + * via `SerialIter`. The result is in wire-canonical form: all equivalent + * in-memory representations collapse to the same byte sequence, field + * ordering is normalized, and the transaction ID is freshly computed. + * + * Any code that synthesizes a transaction from JSON or via the programmatic + * assembler constructor and then submits it to the consensus pipeline should + * call `sterilize` first. + * + * @param stx The source transaction to sterilize. + * @return A `shared_ptr` to the newly constructed canonical `STTx const`. + * @throws std::runtime_error if the round-trip deserialization fails. + */ std::shared_ptr sterilize(STTx const& stx); -/** Check whether a transaction is a pseudo-transaction */ +/** Determine whether a transaction object is a ledger-generated pseudo-transaction. + * + * Pseudo-transactions (`ttAMENDMENT`, `ttFEE`, `ttUNL_MODIFY`) are + * synthesized internally by the ledger and must never be submitted by + * external clients. `passesLocalChecks` rejects any object for which this + * returns `true`. + * + * @param tx The transaction object to test; need not be a fully constructed + * `STTx`. + * @return `true` if the object carries a pseudo-transaction type. + */ bool isPseudoTx(STObject const& tx); diff --git a/include/xrpl/protocol/STValidation.h b/include/xrpl/protocol/STValidation.h index 0b7f53eb55..a976f9021f 100644 --- a/include/xrpl/protocol/STValidation.h +++ b/include/xrpl/protocol/STValidation.h @@ -1,3 +1,19 @@ +/** @file + * Defines `STValidation`, the wire-format object for a single ledger + * validation message in the XRPL consensus protocol. + * + * Validators broadcast one of these objects each consensus round to signal + * agreement on a specific closed ledger. Peers deserialize inbound messages + * into `STValidation` instances, verify signatures, and count them toward + * quorum. The class therefore has two distinct construction paths: one for + * creation-and-signing by the local validator, one for deserialization of a + * peer's message. See the two constructors for details. + * + * `STValidation` is owned via `std::shared_ptr` and wrapped by `RCLValidation` + * in the consensus machinery; that adapter provides the concept interface + * expected by the generic quorum-counting engine without coupling this class + * to consensus-specific logic. + */ #pragma once #include @@ -13,14 +29,45 @@ namespace xrpl { -// Validation flags +// --- Wire flag constants (stored in sfFlags; part of the signed payload) --- -// This is a full (as opposed to a partial) validation +/** Bit flag indicating a full (as opposed to a partial) validation. + * + * A partial validation signals participation in the consensus round without + * fully endorsing a specific ledger hash. Validators set this flag when they + * have applied the consensus transaction set and validated the resulting + * ledger. Read via `isFull()`. + */ constexpr std::uint32_t kVF_FULL_VALIDATION = 0x00000001; -// The signature is fully canonical +/** Bit flag indicating that the DER-encoded signature uses the low-S canonical form. + * + * XRPL requires low-S ECDSA signatures to prevent signature malleability. + * The signing constructor always sets this flag, and `isValid()` passes it + * to `verifyDigest()` to enforce canonicality on inbound messages. Because + * this value is stored in `sfFlags` inside the signed payload, it cannot be + * toggled without invalidating the signature. + */ constexpr std::uint32_t kVF_FULLY_CANONICAL_SIG = 0x80000000; +/** Wire-format representation of a ledger validation message in XRPL consensus. + * + * Inherits from `STObject` for typed-field serialization (the same system + * used by transactions and ledger entries) and from `CountedObject` for + * live-instance tracking in a long-running `rippled` process. + * + * The class maintains two separate concepts that must not be conflated: + * - **Validity** (`valid_`): whether the cryptographic signature is correct. + * Lazily evaluated and cached on first call to `isValid()`. + * - **Trust** (`trusted_`): whether the issuing validator is on this node's + * current Unique Node List (UNL). Set via `setTrusted()`/`setUntrusted()`; + * can change at runtime as the UNL evolves. + * + * @note Only `secp256k1` signing keys are accepted. Passing an `Ed25519` + * public key to either constructor throws at construction time. + * @see RCLValidation — the adapter that exposes this object to the generic + * consensus engine. + */ class STValidation final : public STObject, public CountedObject { bool trusted_ = false; @@ -39,30 +86,65 @@ class STValidation final : public STObject, public CountedObject NetClock::time_point seenTime_; public: - /** Construct a STValidation from a peer from serialized data. - - @param sit Iterator over serialized data - @param lookupNodeID Invocable with signature - NodeID(PublicKey const&) - used to find the Node ID based on the public key - that signed the validation. For manifest based - validators, this should be the NodeID of the master - public key. - @param checkSignature Whether to verify the data was signed properly - - @note Throws if the object is not valid - */ + /** Deserialize a validation received from a peer. + * + * Parses the binary payload via `STObject`, then extracts the signing + * public key from `sfSigningPubKey`. The `lookupNodeID` callable + * translates the ephemeral signing key to the validator's stable master + * `NodeID` (which may differ when the validator has rotated its ephemeral + * key via the manifest mechanism). + * + * @tparam LookupNodeID Callable with signature `NodeID(PublicKey const&)`. + * For manifest-based validators this should resolve to the master key's + * `NodeID`; for static-key validators it is typically + * `calcNodeID(pk)`. + * @param sit Iterator over the raw serialized validation bytes. + * @param lookupNodeID Invocable that maps the signing `PublicKey` to a + * stable `NodeID` used for UNL membership checks. + * @param checkSignature If `true`, verifies the signature immediately and + * throws on failure. Pass `false` to defer verification to the first + * call of `isValid()` (the pattern used by `PeerImp` to avoid + * synchronous cryptographic work on the peer-message path). + * @throws std::runtime_error if the serialized data is malformed, if the + * signing public key is absent or not a `secp256k1` key, or if + * `checkSignature` is `true` and the signature does not verify. + * @note After construction `seenTime_` is zero; callers must call + * `setSeen()` to record local receipt time before storing the object. + */ template STValidation(SerialIter& sit, LookupNodeID&& lookupNodeID, bool checkSignature); - /** Construct, sign and trust a new STValidation issued by this node. - - @param signTime When the validation is signed - @param publicKey The current signing public key - @param secretKey The current signing secret key - @param nodeID ID corresponding to node's public master key - @param f callback function to "fill" the validation with necessary data - */ + /** Construct, sign, and trust a new validation issued by the local node. + * + * Sets mandatory bookkeeping fields (`sfSigningPubKey`, `sfSigningTime`), + * invokes the filler callback `f(*this)` so the caller can attach + * optional fields (ledger hash, consensus hash, fee votes, amendment + * bits, server version), then signs the result with `signDigest` and + * marks the object as trusted. The `kVF_FULLY_CANONICAL_SIG` flag is + * always set, enforcing low-S ECDSA on the produced signature. + * + * After `f` returns a format-validation sweep checks that all + * `SoeRequired` fields are present; a missing required field is a + * programming error and triggers `logicError`. + * + * `seenTime_` is initialized to `signTime`, making sign time and seen + * time identical for locally created validations. + * + * @tparam F Callable with signature `void(STValidation&)`. + * @param signTime The time at which the validation is being signed; + * stored in `sfSigningTime` and used as the initial `seenTime_`. + * @param pk The validator's current ephemeral signing public key. + * Must be a `secp256k1` key; passing any other type calls + * `logicError`. + * @param sk The secret key matching `pk`, used to produce `sfSignature`. + * @param nodeID The stable master-key `NodeID` of this validator. + * @param f Callback invoked after mandatory fields are set but before + * signing. Use it to populate `sfLedgerHash`, `sfLedgerSequence`, + * `sfConsensusHash`, `sfFlags`, and any optional advisory fields. + * @note The resulting object is immediately marked as trusted and + * `valid_` is set to `true` without re-verifying, since the node + * just produced the signature. + */ template STValidation( NetClock::time_point signTime, @@ -71,53 +153,171 @@ public: NodeID const& nodeID, F&& f); - // Hash of the validated ledger + /** Return the hash of the ledger this validation endorses. + * + * @return Value of the `sfLedgerHash` field. + */ uint256 getLedgerHash() const; - // Hash of consensus transaction set used to generate ledger + /** Return the hash of the consensus transaction set that produced the validated ledger. + * + * @return Value of the `sfConsensusHash` field. Returns a zero hash if + * the field is absent (the field is optional in the schema). + */ uint256 getConsensusHash() const; + /** Return the time at which the validator claims to have signed this validation. + * + * Reads `sfSigningTime` from the serialized payload; because that field + * is part of the signed content it cannot be forged without invalidating + * the signature. Note that this is the validator's own clock time and + * may differ from `getSeenTime()`, which is when the *local* node + * received the message. + * + * @return The signing instant as a `NetClock::time_point`. + */ NetClock::time_point getSignTime() const; + /** Return the local time at which this node received or created the validation. + * + * For peer-sourced validations this is set via `setSeen()` after receipt. + * For self-issued validations the constructor initializes it to `signTime`. + * This value is never serialized or sent over the wire. + * + * @return The local receipt time as a `NetClock::time_point`. + */ NetClock::time_point getSeenTime() const noexcept; + /** Return the ephemeral public key that signed this validation. + * + * May differ from the validator's stable master key when the validator + * has rotated its signing key via the manifest mechanism. + * + * @return A reference to the immutable `signingPubKey_`. + */ PublicKey const& getSignerPublic() const noexcept; + /** Return the stable master-key `NodeID` of the issuing validator. + * + * For manifest-based validators this is derived from the master public + * key rather than the ephemeral signing key. It is the identity used + * for UNL membership checks and quorum counting. + * + * @return A reference to the immutable `nodeID_`. + */ NodeID const& getNodeID() const noexcept; + /** Verify the cryptographic signature, caching the result for future calls. + * + * On the first call, verifies the ECDSA signature over `getSigningHash()` + * using `signingPubKey_`. The `kVF_FULLY_CANONICAL_SIG` flag is consulted + * to enforce low-S canonicality. The result is stored in `valid_` and + * returned on all subsequent calls without re-computing. + * + * For self-issued validations the signing constructor pre-sets + * `valid_ = true`, so this method never performs cryptographic work. + * + * @return `true` if the signature is valid; `false` otherwise. + */ bool isValid() const noexcept; + /** Return whether this is a full (as opposed to partial) validation. + * + * A full validation endorses a specific ledger hash. A partial validation + * only signals that the validator participated in the round. + * + * @return `true` if the `kVF_FULL_VALIDATION` bit is set in `sfFlags`. + */ bool isFull() const noexcept; + /** Return whether this validation is marked as trusted by the local node. + * + * Trust reflects whether the issuing validator is on this node's current + * UNL and is independent of cryptographic validity. Self-issued + * validations are always trusted from construction. + * + * @return The current value of the `trusted_` flag. + */ bool isTrusted() const noexcept; + /** Compute the domain-separated hash that was (or will be) signed. + * + * Prepends `HashPrefix::Validation` (`'V','A','L',0x00`) to the canonical + * serialization of all signed fields, then applies SHA-512-Half. The + * prefix prevents a validation hash from colliding with any other signed + * payload type (transactions, proposals, etc.). + * + * @return The 256-bit signing digest. + */ uint256 getSigningHash() const; + /** Mark this validation as trusted. + * + * Called when the issuing validator is confirmed to be on this node's + * current UNL. May be called multiple times; subsequent calls are no-ops. + */ void setTrusted(); + /** Mark this validation as untrusted. + * + * Called when the issuing validator is removed from this node's current + * UNL, or when the validation is being re-evaluated. Does not affect the + * cryptographic `valid_` cache. + */ void setUntrusted(); + /** Record the local time at which this node received the validation. + * + * Should be called immediately after constructing a peer-sourced + * validation, before the object is stored or forwarded. For self-issued + * validations the signing constructor sets this to `signTime` + * automatically. + * + * @param s The local receipt time. + */ void setSeen(NetClock::time_point s); + /** Serialize this validation to its complete binary wire format. + * + * The returned bytes include all fields, including `sfSignature`, and are + * suitable for network transmission or deduplication hashing. To suppress + * relay of a duplicate message, callers typically hash this output with + * `sha512Half`. + * + * @return A `Blob` containing the complete serialized validation. + */ Blob getSerialized() const; + /** Return the raw DER-encoded ECDSA signature from the serialized payload. + * + * @return Value of the `sfSignature` field as a `Blob`. + */ Blob getSignature() const; + /** Produce a human-readable summary of this validation for logging. + * + * Renders all major fields (ledger hash, consensus hash, sign/seen times, + * signer public key, node ID, validity, fullness, trust status, signing + * hash, and Base58-encoded public key) into a single-line string. + * + * @return A diagnostic string; not suitable for machine parsing or + * network transmission. + */ std::string render() const { @@ -134,6 +334,12 @@ public: } private: + /** Return the field schema for `STValidation` objects. + * + * Function-local static to guarantee that all `SField` singletons are + * initialized before the `SOTemplate` is constructed (C++ provides no + * cross-translation-unit initialization order for namespace-scope statics). + */ static SOTemplate const& validationFormat(); @@ -168,14 +374,6 @@ STValidation::STValidation(SerialIter& sit, LookupNodeID&& lookupNodeID, bool ch XRPL_ASSERT(nodeID_.isNonZero(), "xrpl::STValidation::STValidation(SerialIter) : nonzero node"); } -/** Construct, sign and trust a new STValidation issued by this node. - - @param signTime When the validation is signed - @param publicKey The current signing public key - @param secretKey The current signing secret key - @param nodeID ID corresponding to node's public master key - @param f callback function to "fill" the validation with necessary data -*/ template STValidation::STValidation( NetClock::time_point signTime, diff --git a/include/xrpl/protocol/STVector256.h b/include/xrpl/protocol/STVector256.h index ab3a2f99e3..b5c050bb35 100644 --- a/include/xrpl/protocol/STVector256.h +++ b/include/xrpl/protocol/STVector256.h @@ -1,3 +1,11 @@ +/** @file + * Declares STVector256, the serialized type for ordered lists of uint256 values. + * + * On the wire the array is encoded as a single VL-prefixed blob of concatenated + * 32-byte hashes (type identifier STI_VECTOR256, code 19). Common ledger fields + * that use this type include sfAmendments, sfIndexes, and sfHashes. + */ + #pragma once #include @@ -7,92 +15,268 @@ namespace xrpl { +/** Serialized type for an ordered list of 256-bit hash values. + * + * Wraps a `std::vector` with the `STBase` contract so that hash + * collections can be stored as named, typed fields inside `STObject` — + * giving them a wire-format identity (`STI_VECTOR256`, code 19), a + * canonical binary encoding (VL-prefixed blob of packed 32-byte values), + * and a JSON representation (array of hex strings). Typical ledger uses + * include `sfAmendments` (active amendments in a validator vote), + * `sfIndexes` (keys in a `DirectoryNode` page), and `sfHashes`. + * + * An empty `STVector256` is the canonical default state and is omitted from + * the wire encoding when the field is declared optional. + * + * The `CountedObject` mixin adds lock-free instance counting + * for diagnostic purposes, with no overhead in the fast path. + */ class STVector256 : public STBase, public CountedObject { std::vector value_; public: + /** Reference type used when this value is passed as a read-only handle. */ using value_type = std::vector const&; + /** Construct an empty, unnamed STVector256. */ STVector256() = default; + /** Construct an empty STVector256 bound to the given field name. + * + * @param n The SField that identifies this field in its parent STObject. + */ explicit STVector256(SField const& n); + + /** Construct an unnamed STVector256 pre-populated with @p vector. + * + * @param vector Initial contents; copied into the internal store. + */ explicit STVector256(std::vector const& vector); + + /** Construct an STVector256 bound to @p n and pre-populated with @p vector. + * + * @param n The SField that identifies this field in its parent STObject. + * @param vector Initial contents; copied into the internal store. + */ STVector256(SField const& n, std::vector const& vector); + + /** Deserialize an STVector256 from a wire-format stream. + * + * Reads a single VL-prefixed blob from @p sit and partitions it into + * consecutive 32-byte chunks, each becoming one `uint256` entry. The + * resulting vector retains the original wire order. + * + * @param sit Forward-only iterator positioned at the VL length prefix of + * the field. Consumed by exactly one VL-length + slice read pair. + * @param name The SField that identifies this field within its parent STObject. + * @throws std::runtime_error if the decoded blob length is not an exact + * multiple of 32 bytes, indicating corrupt or truncated data. + */ STVector256(SerialIter& sit, SField const& name); + /** Return the serialized type identifier for this field. + * + * @return `STI_VECTOR256` (code 19). + */ [[nodiscard]] SerializedTypeID getSType() const override; + /** Serialize this array into @p s as a VL-prefixed blob. + * + * Writes a length prefix followed by the concatenated raw bytes of each + * `uint256` entry (32 bytes per element, no padding or separators). + * + * @param s Accumulator to append the encoded field into. + * @note Asserts (debug builds only) that the associated SField is marked + * binary and carries type `STI_VECTOR256`. These guards catch accidental + * field-type mismatches before data reaches the wire. + */ void add(Serializer& s) const override; + /** Produce a JSON array of hex-encoded hash strings. + * + * Each `uint256` entry is rendered as a lowercase hex string via + * `to_string()`. The @p options parameter is accepted for interface + * conformance but is unused; the representation is identical across + * all API versions. + * + * @return A `json::arrayValue` with one hex string per entry. + */ [[nodiscard]] json::Value getJson(JsonOptions) const override; + /** Test deep equality with another STBase instance. + * + * Two `STVector256` objects are equivalent when they contain the same + * sequence of `uint256` values in the same order. + * + * @param t The object to compare against. + * @return `true` if @p t is an `STVector256` with identical contents; + * `false` if the types differ or the sequences do not match. + */ [[nodiscard]] bool isEquivalent(STBase const& t) const override; + /** Return whether this object holds no entries. + * + * An empty `STVector256` is the canonical default value. Per XRPL + * serialization rules, default-valued optional fields are omitted from + * the wire encoding and contribute nothing to a transaction or ledger hash. + * + * @return `true` if the internal vector is empty. + */ [[nodiscard]] bool isDefault() const override; + /** Replace the contents with a copy of @p v. + * + * @param v Source vector; copied into the internal store. + * @return Reference to this object. + */ STVector256& operator=(std::vector const& v); + /** Replace the contents by moving @p v into the internal store. + * + * @param v Source vector; left in a valid but unspecified state after the call. + * @return Reference to this object. + */ STVector256& operator=(std::vector&& v); + /** Copy the inner vector from @p v, leaving the SField name unchanged. + * + * Unlike `operator=`, this copies only the payload (`mValue`), not the + * field binding. Use this when you need to transfer values between two + * fields that have different SField identities. + * + * @param v Source object whose contents are copied. + */ void setValue(STVector256 const& v); - /** Retrieve a copy of the vector we contain */ + /** Return a copy of the internal vector. + * + * Marked `explicit` to prevent accidental implicit copies in generic + * contexts; prefer `value()` for read-only access. + */ explicit operator std::vector() const; + /** Return the number of entries in the vector. + * + * @return Entry count; 0 for an empty (default) object. + */ [[nodiscard]] std::size_t size() const; + /** Resize the internal vector to @p n entries. + * + * New entries (if any) are value-initialized to the zero `uint256`. + * + * @param n Target size. + */ void resize(std::size_t n); + /** Return whether the vector contains no entries. + * + * @return `true` if `size() == 0`. + */ [[nodiscard]] bool empty() const; + /** Return a mutable reference to the entry at index @p n. + * + * @param n Zero-based index; behavior is undefined if out of range. + */ std::vector::reference operator[](std::vector::size_type n); + /** Return a read-only reference to the entry at index @p n. + * + * @param n Zero-based index; behavior is undefined if out of range. + */ std::vector::const_reference operator[](std::vector::size_type n) const; + /** Return a read-only reference to the internal vector. + * + * Prefer this over the explicit conversion operator for non-owning access. + * + * @return Const reference to the underlying `std::vector`. + */ [[nodiscard]] std::vector const& value() const; + /** Insert @p value before @p pos. + * + * @param pos Iterator before which the new element is inserted. + * @param value Hash to insert. + * @return Iterator to the inserted element. + */ std::vector::iterator insert(std::vector::const_iterator pos, uint256 const& value); + /** Append @p v to the end of the vector. + * + * @param v Hash to append. + */ void pushBack(uint256 const& v); + /** Return a mutable iterator to the first element. */ std::vector::iterator begin(); + /** Return a read-only iterator to the first element. */ [[nodiscard]] std::vector::const_iterator begin() const; + /** Return a mutable past-the-end iterator. */ std::vector::iterator end(); + /** Return a read-only past-the-end iterator. */ [[nodiscard]] std::vector::const_iterator end() const; + /** Remove the element at @p position. + * + * @param position Iterator to the element to remove. + * @return Iterator to the element following the removed one. + */ std::vector::iterator erase(std::vector::iterator position); + /** Remove all entries, leaving the vector empty. */ void clear() noexcept; private: + /** Copy this object into a caller-supplied buffer via the STVar placement protocol. + * + * Called only by `detail::STVar`. Delegates to `STBase::emplace`, which + * constructs in-place when @p buf is large enough, or heap-allocates otherwise. + * + * @param n Size of the buffer at @p buf, in bytes. + * @param buf Destination buffer for placement construction. + * @return Pointer to the newly constructed `STVector256`. + */ STBase* copy(std::size_t n, void* buf) const override; + + /** Move this object into a caller-supplied buffer via the STVar placement protocol. + * + * Called only by `detail::STVar`. Delegates to `STBase::emplace`, which + * constructs in-place when @p buf is large enough, or heap-allocates otherwise. + * The source is left in a valid but unspecified state. + * + * @param n Size of the buffer at @p buf, in bytes. + * @param buf Destination buffer for placement construction. + * @return Pointer to the newly constructed `STVector256`. + */ STBase* move(std::size_t n, void* buf) override; @@ -132,7 +316,6 @@ STVector256::setValue(STVector256 const& v) value_ = v.value_; } -/** Retrieve a copy of the vector we contain */ inline STVector256:: operator std::vector() const { diff --git a/include/xrpl/protocol/STXChainBridge.h b/include/xrpl/protocol/STXChainBridge.h index 292ffe2767..71ce5b33f7 100644 --- a/include/xrpl/protocol/STXChainBridge.h +++ b/include/xrpl/protocol/STXChainBridge.h @@ -10,6 +10,26 @@ namespace xrpl { class Serializer; class STObject; +/** Serialized type encoding the four-field specification of an XRPL cross-chain bridge. + * + * A bridge connects two independent ledgers: a *locking chain* (where XRP or + * tokens are held in escrow) and an *issuing chain* (where a wrapped + * representation is minted). Each side is described by a door account + * (`AccountID`) and an asset (`Issue`). This class bundles those four pieces + * — `LockingChainDoor`, `LockingChainIssue`, `IssuingChainDoor`, + * `IssuingChainIssue` — into a single, typed, wire-format ledger field that + * appears in bridge-related transactions and ledger entries. + * + * Inherits `STBase` (type-ID `STI_XCHAIN_BRIDGE`) for polymorphic + * serialization and `CountedObject` for debug instance tracking. + * + * Both `operator==` and `operator<` compare all four fields in declaration + * order via `std::tie`, making `STXChainBridge` usable as a key in ordered + * associative containers. + * + * @see XChainAttestations.h for how bridges are consumed by witness and + * attestation logic. + */ class STXChainBridge final : public STBase, public CountedObject { STAccount lockingChainDoor_{sfLockingChainDoor}; @@ -18,80 +38,240 @@ class STXChainBridge final : public STBase, public CountedObject STIssue issuingChainIssue_{sfIssuingChainIssue}; public: + /** Self-alias used by template code that calls `.value()` to strip the + * ST wrapper; for compound types the value type is the type itself. */ using value_type = STXChainBridge; + /** Identifies which of the two ledgers in a bridge a given door or asset + * belongs to. */ enum class ChainType { Locking, Issuing }; + /** Returns the chain that is opposite to @p ct. + * + * @param ct The chain whose counterpart is requested. + * @return `ChainType::Issuing` when @p ct is `Locking`; `Locking` + * otherwise. + */ static ChainType otherChain(ChainType ct); + /** Maps the witness `wasLockingChainSend` flag to the originating chain. + * + * Normalizes a boolean attestation flag into a `ChainType`, removing + * scattered `if (wasLockingChainSend)` branches from callers. + * + * @param wasLockingChainSend `true` when the send originated on the + * locking chain. + * @return `ChainType::Locking` when @p wasLockingChainSend is `true`; + * `ChainType::Issuing` otherwise. + */ static ChainType srcChain(bool wasLockingChainSend); + /** Maps the witness `wasLockingChainSend` flag to the destination chain. + * + * Complement of `srcChain()`: returns the chain that receives the assets. + * + * @param wasLockingChainSend `true` when the send originated on the + * locking chain. + * @return `ChainType::Issuing` when @p wasLockingChainSend is `true`; + * `ChainType::Locking` otherwise. + */ static ChainType dstChain(bool wasLockingChainSend); + /** Constructs an empty bridge bound to `sfXChainBridge`. + * + * Used as a canonical reference instance (e.g., to obtain the known + * JSON key set for extra-field detection in the JSON constructor). + */ STXChainBridge(); + /** Constructs an empty bridge bound to the given field name. + * + * @param name The `SField` tag to associate with this object inside an + * enclosing `STObject`. + */ explicit STXChainBridge(SField const& name); STXChainBridge(STXChainBridge const& rhs) = default; + /** Extracts bridge sub-fields from an already-parsed generic `STObject`. + * + * Used during ledger deserialization when the parent has been parsed as + * an `STObject` and the four bridge fields must be projected into the + * strongly-typed form. + * + * @param o Source object; must contain `sfLockingChainDoor`, + * `sfLockingChainIssue`, `sfIssuingChainDoor`, and + * `sfIssuingChainIssue` — `STObject::operator[]` throws `FieldErr` + * if any field is absent. + */ STXChainBridge(STObject const& o); + /** Constructs a bridge from its four constituent values. + * + * @param srcChainDoor Door account on the locking chain. + * @param srcChainIssue Asset locked or released on the locking chain. + * @param dstChainDoor Door account on the issuing chain. + * @param dstChainIssue Wrapped asset minted or burned on the issuing chain. + */ STXChainBridge( AccountID const& srcChainDoor, Issue const& srcChainIssue, AccountID const& dstChainDoor, Issue const& dstChainIssue); + /** Deserializes a bridge from a JSON object, binding to `sfXChainBridge`. + * + * Delegates to the two-argument form with `sfXChainBridge`. + * + * @param v JSON object with keys `LockingChainDoor`, `LockingChainIssue`, + * `IssuingChainDoor`, `IssuingChainIssue`. + * @throws std::runtime_error if @p v is not an object, contains + * unrecognized keys, or either door field is not a valid Base58-encoded + * account. + */ explicit STXChainBridge(json::Value const& v); + /** Deserializes a bridge from a JSON object, binding to @p name. + * + * Performs a strict whitelist check against the canonical key set from a + * default-constructed bridge before parsing any values, rejecting typos + * and unknown fields at parse time rather than silently ignoring them. + * + * @param name The `SField` to associate with this object. + * @param v JSON object with the four bridge fields. + * @throws std::runtime_error if @p v is not an object, contains any key + * absent from the canonical set, or either door is not a valid + * Base58-encoded account. + */ explicit STXChainBridge(SField const& name, json::Value const& v); + /** Deserializes a bridge from a binary stream. + * + * Hot path for on-disk and network deserialization. Reads the four + * sub-fields in canonical order: locking door, locking issue, issuing + * door, issuing issue. Each sub-field consumes its own field-ID header + * and payload bytes from @p sit. + * + * @param sit Forward-only cursor positioned at the first byte of the + * bridge payload; advanced past all four fields on return. + * @param name The `SField` to associate with this object. + */ explicit STXChainBridge(SerialIter& sit, SField const& name); STXChainBridge& operator=(STXChainBridge const& rhs) = default; + /** Returns a human-readable representation of the bridge for diagnostics. + * + * Format: `{ LockingChainDoor = , LockingChainIssue = , + * IssuingChainDoor = , IssuingChainIssue = }`. + * + * @return Formatted string; intended for logging and debug output only. + */ [[nodiscard]] std::string getText() const override; + /** Converts this bridge into a generic `STObject` with the same four fields. + * + * Needed when the bridge must participate in code paths that operate on + * `STObject` graphs, such as transaction metadata construction. + * + * @return A new `STObject` bound to `sfXChainBridge` containing copies of + * all four bridge sub-fields. + */ [[nodiscard]] STObject toSTObject() const; + /** Returns the door account of the locking chain. */ [[nodiscard]] AccountID const& lockingChainDoor() const; + /** Returns the asset locked or released on the locking chain. */ [[nodiscard]] Issue const& lockingChainIssue() const; + /** Returns the door account of the issuing chain. */ [[nodiscard]] AccountID const& issuingChainDoor() const; + /** Returns the wrapped asset minted or burned on the issuing chain. */ [[nodiscard]] Issue const& issuingChainIssue() const; + /** Returns the door account for the specified chain. + * + * Allows generic code (e.g., attestation handlers) to query either side + * of a bridge without hard-coding which chain is locking vs. issuing. + * Pair with `srcChain()`/`dstChain()` to map a `wasLockingChainSend` + * boolean to the correct `ChainType`. + * + * @param ct Which side of the bridge to query. + * @return The locking-chain door when @p ct is `Locking`; the + * issuing-chain door otherwise. + */ [[nodiscard]] AccountID const& door(ChainType ct) const; + /** Returns the asset for the specified chain. + * + * @param ct Which side of the bridge to query. + * @return The locking-chain issue when @p ct is `Locking`; the + * issuing-chain issue otherwise. + */ [[nodiscard]] Issue const& issue(ChainType ct) const; + /** Returns `STI_XCHAIN_BRIDGE`, the type discriminator for this ST class. */ [[nodiscard]] SerializedTypeID getSType() const override; + /** Serializes the bridge to JSON. + * + * Produces an object with keys `LockingChainDoor`, `LockingChainIssue`, + * `IssuingChainDoor`, `IssuingChainIssue`. The canonical key set from + * this output is also used by the JSON constructor to detect extra fields. + * + * @return JSON object representation of all four bridge fields. + */ [[nodiscard]] json::Value getJson(JsonOptions) const override; + /** Appends the binary encoding of all four sub-fields to @p s. + * + * Each sub-field is written in canonical declaration order (locking door, + * locking issue, issuing door, issuing issue) and includes its own + * field-ID header, mirroring the `SerialIter` constructor's read order. + * + * @param s Serializer accumulator to append to. + */ void add(Serializer& s) const override; + /** Polymorphic equality check used by `STBase` container comparisons. + * + * Performs a `dynamic_cast` to `STXChainBridge` and delegates to + * `operator==`. Returns `false` if @p t is not an `STXChainBridge`. + * + * @param t The object to compare against. + * @return `true` iff @p t is an `STXChainBridge` with identical fields. + */ [[nodiscard]] bool isEquivalent(STBase const& t) const override; + /** Returns `true` when all four sub-fields are in their default state. */ [[nodiscard]] bool isDefault() const override; + /** Returns a reference to this object itself. + * + * Satisfies the convention that template code calling `.value()` on an + * ST type receives the unwrapped value. For compound types like + * `STXChainBridge`, `value_type` equals the type itself. + * + * @return `*this`. + */ [[nodiscard]] value_type const& value() const noexcept; @@ -111,6 +291,11 @@ private: operator<(STXChainBridge const& lhs, STXChainBridge const& rhs); }; +/** Returns `true` iff the two bridges have identical door accounts and assets. + * + * Comparison is performed via `std::tie` across all four fields in + * declaration order: locking door, locking issue, issuing door, issuing issue. + */ inline bool operator==(STXChainBridge const& lhs, STXChainBridge const& rhs) { @@ -126,6 +311,11 @@ operator==(STXChainBridge const& lhs, STXChainBridge const& rhs) rhs.issuingChainIssue_); } +/** Strict weak ordering over bridges; enables use as a `std::map`/`std::set` key. + * + * Comparison is performed via `std::tie` across all four fields in + * declaration order: locking door, locking issue, issuing door, issuing issue. + */ inline bool operator<(STXChainBridge const& lhs, STXChainBridge const& rhs) { diff --git a/include/xrpl/protocol/SecretKey.h b/include/xrpl/protocol/SecretKey.h index 9af27e9709..f5b56d2aca 100644 --- a/include/xrpl/protocol/SecretKey.h +++ b/include/xrpl/protocol/SecretKey.h @@ -13,10 +13,27 @@ namespace xrpl { -/** A secret key. */ +/** A 32-byte private key for either secp256k1 or Ed25519. + * + * The destructor unconditionally zeroes the backing buffer via `secureErase`, + * defending against cold-boot and memory-dump attacks. Intermediate buffers + * in all key-generation and derivation helpers are likewise erased. + * + * Comparison operators are deleted: comparing secret keys in application code + * is almost always a mistake (compare public keys or `AccountID`s instead), + * and any comparison implementation risks timing-observable branches that + * could leak key material through a side channel. + * + * `operator<<` is absent by design — streaming a secret key to a log or debug + * output is too easy an accident. Use `toString()` for the rare legitimate case. + * + * @note The default constructor is deleted; a `SecretKey` must always be + * initialised with actual key material. + */ class SecretKey { public: + /** Size of the raw key buffer in bytes. */ static constexpr std::size_t kSIZE = 32; private: @@ -30,54 +47,78 @@ public: SecretKey& operator=(SecretKey const&) = default; + /** Deleted: comparing secret keys risks timing side-channel leaks. */ bool operator==(SecretKey const&) = delete; + + /** Deleted: comparing secret keys risks timing side-channel leaks. */ bool operator!=(SecretKey const&) = delete; + /** Zeroes the key buffer via `secureErase` before releasing memory. */ ~SecretKey(); + /** Construct from a 32-byte array. + * + * @param data Raw key material; copied into the internal buffer. + */ SecretKey(std::array const& data); + + /** Construct from a `Slice`. + * + * @param slice Raw key material; must be exactly 32 bytes. + * @throws LogicError if `slice.size() != 32`. + */ SecretKey(Slice const& slice); + /** @return Pointer to the first byte of the raw 32-byte key material. */ [[nodiscard]] std::uint8_t const* data() const { return buf_; } + /** @return Number of bytes in the key buffer (always 32). */ [[nodiscard]] std::size_t size() const { return sizeof(buf_); } - /** Convert the secret key to a hexadecimal string. - - @note The operator<< function is deliberately omitted - to avoid accidental exposure of secret key material. - */ + /** Return the key as a hexadecimal string. + * + * Use this only where the hex representation is genuinely required + * (e.g. CLI tooling). Prefer keeping the key in its binary form + * everywhere else. `operator<<` is intentionally absent to prevent + * accidental exposure in log output. + * + * @return Hex-encoded string of the 32-byte key. + */ [[nodiscard]] std::string toString() const; + /** @return Iterator to the first byte of the key buffer. */ [[nodiscard]] const_iterator begin() const noexcept { return buf_; } + /** @return Iterator to the first byte of the key buffer. */ [[nodiscard]] const_iterator cbegin() const noexcept { return buf_; } + /** @return Past-the-end iterator for the key buffer. */ [[nodiscard]] const_iterator end() const noexcept { return buf_ + sizeof(buf_); } + /** @return Past-the-end iterator for the key buffer. */ [[nodiscard]] const_iterator cend() const noexcept { @@ -85,61 +126,157 @@ public: } }; +/** Deleted: comparing secret keys risks timing side-channel leaks. */ bool operator==(SecretKey const& lhs, SecretKey const& rhs) = delete; +/** Deleted: comparing secret keys risks timing side-channel leaks. */ bool operator!=(SecretKey const& lhs, SecretKey const& rhs) = delete; //------------------------------------------------------------------------------ -/** Parse a secret key */ +/** Decode a Base58Check-encoded secret key. + * + * Decodes the token and validates that the payload is exactly 32 bytes. + * Never throws — returns `std::nullopt` on decoding failure or length + * mismatch. + * + * @param type The expected `TokenType` prefix (e.g. `TokenType::FamilySeed`). + * @param s Base58Check-encoded string to decode. + * @return The decoded `SecretKey`, or `std::nullopt` on any error. + */ template <> std::optional parseBase58(TokenType type, std::string const& s); +/** Encode a secret key as a Base58Check string. + * + * The `TokenType` argument controls the version byte prepended during + * encoding, consistent with the XRPL token system (e.g. `TokenType::FamilySeed`). + * + * @param type Version byte selector for the Base58Check envelope. + * @param sk The secret key to encode. + * @return Base58Check-encoded string. + */ inline std::string toBase58(TokenType type, SecretKey const& sk) { return encodeBase58Token(type, sk.data(), sk.size()); } -/** Create a secret key using secure random numbers. */ +/** Generate a secret key from the platform CSPRNG. + * + * Fills 32 bytes from `crypto_prng()`, constructs the key, then immediately + * erases the temporary stack buffer. The result is not tied to any seed and + * cannot be deterministically reproduced — use `generateKeyPair` when wallet + * recovery is required. + * + * @return A freshly generated `SecretKey` backed by cryptographically secure + * random bytes. + */ SecretKey randomSecretKey(); -/** Generate a new secret key deterministically. */ +/** Derive a secret key deterministically from a seed. + * + * - **Ed25519**: the secret key is `sha512Half(seed)` directly. + * - **secp256k1**: hashes `seed || counter` with SHA512-Half, retrying with + * an incrementing counter until the result is a valid curve scalar. In + * practice this loop almost never executes more than once. + * + * All intermediate key-material buffers are erased before return. + * + * @param type Algorithm (`KeyType::Ed25519` or `KeyType::Secp256k1`). + * @param seed The 128-bit XRPL seed. + * @return The derived `SecretKey`. + * @throws std::runtime_error (secp256k1 only) if no valid scalar is found + * within 128 attempts (statistically negligible). + */ SecretKey generateSecretKey(KeyType type, Seed const& seed); -/** Derive the public key from a secret key. */ +/** Derive the public key corresponding to a secret key. + * + * - **secp256k1**: produces a 33-byte compressed curve point. + * - **Ed25519**: produces a 33-byte key where `buf[0] == 0xED` followed by + * the 32-byte Edwards-curve public key. The `0xED` prefix is the XRPL + * wire convention that `publicKeyType()` uses to distinguish Ed25519 keys + * from secp256k1 keys (which start with `0x02` or `0x03`). + * + * @param type Algorithm (`KeyType::Ed25519` or `KeyType::Secp256k1`). + * @param sk The secret key to derive from. + * @return The corresponding `PublicKey`. + */ PublicKey derivePublicKey(KeyType type, SecretKey const& sk); -/** Generate a key pair deterministically. - - This algorithm is specific to the XRPL: - - For secp256k1 key pairs, the seed is converted - to a Generator and used to compute the key pair - corresponding to ordinal 0 for the generator. -*/ +/** Generate a key pair deterministically from a seed. + * + * This is the main entry point for wallet-style key derivation. + * + * - **secp256k1**: uses XRPL's custom two-level derivation algorithm (which + * predates BIP-32). A root private key is derived from the seed, its + * compressed public key becomes the "generator point", and the child key + * at ordinal 0 is produced by tweaking the root with a SHA512-Half of + * the generator concatenated with the ordinal. Third-party wallets that + * need to import existing XRPL accounts should support this algorithm. + * - **Ed25519**: equivalent to calling `generateSecretKey` then + * `derivePublicKey` directly; no generator indirection is used. + * + * @param type Algorithm (`KeyType::Ed25519` or `KeyType::Secp256k1`). + * @param seed The 128-bit XRPL seed. + * @return `{PublicKey, SecretKey}` pair. + * @throws std::runtime_error propagated from secp256k1 root-key derivation + * if no valid scalar is found within 128 attempts (statistically + * negligible). + * @see https://xrpl.org/cryptographic-keys.html#secp256k1-key-derivation + */ std::pair generateKeyPair(KeyType type, Seed const& seed); -/** Create a key pair using secure random numbers. */ +/** Generate a key pair from the platform CSPRNG (non-deterministic). + * + * Combines `randomSecretKey()` with `derivePublicKey()`. Unlike + * `generateKeyPair()`, the result cannot be reproduced from any seed. + * Use this when wallet recovery is not needed. + * + * @param type Algorithm (`KeyType::Ed25519` or `KeyType::Secp256k1`). + * @return `{PublicKey, SecretKey}` pair backed by random key material. + */ std::pair randomKeyPair(KeyType type); -/** Generate a signature for a message digest. - This can only be used with secp256k1 since Ed25519's - security properties come, in part, from how the message - is hashed. -*/ +/** Sign a pre-computed digest with a secp256k1 key. + * + * Restricted to secp256k1: Ed25519's security proof depends on how the + * message is hashed internally by the primitive, so pre-hashed signing is + * not supported for Ed25519. Passes a `LogicError` if `pk` is not a + * secp256k1 key. + * + * The ECDSA nonce is generated deterministically per RFC 6979, eliminating + * the class of vulnerabilities caused by weak random nonces. The result is + * DER-encoded and at most 72 bytes. + * + * @param pk Public key; must be secp256k1 (used to verify the key type). + * @param sk Corresponding secret key. + * @param digest The 32-byte SHA512-Half digest to sign. + * @return DER-encoded signature in a `Buffer` (up to 72 bytes). + */ /** @{ */ Buffer signDigest(PublicKey const& pk, SecretKey const& sk, uint256 const& digest); +/** Sign a pre-computed digest, deriving the public key from `type` and `sk`. + * + * Convenience overload that calls `derivePublicKey(type, sk)` internally. + * Restricted to secp256k1 — see the primary overload for details. + * + * @param type Must be `KeyType::Secp256k1`. + * @param sk The secret key to sign with. + * @param digest The 32-byte SHA512-Half digest to sign. + * @return DER-encoded signature in a `Buffer` (up to 72 bytes). + */ inline Buffer signDigest(KeyType type, SecretKey const& sk, uint256 const& digest) { @@ -147,14 +284,35 @@ signDigest(KeyType type, SecretKey const& sk, uint256 const& digest) } /** @} */ -/** Generate a signature for a message. - With secp256k1 signatures, the data is first hashed with - SHA512-Half, and the resulting digest is signed. -*/ +/** Sign a raw message with a key of the detected type. + * + * Dispatches on the key type embedded in `pk`: + * - **Ed25519**: passes the raw message bytes directly to `ed25519_sign`, + * which incorporates its own deterministic internal hashing. Returns a + * fixed 64-byte signature. + * - **secp256k1**: applies SHA512-Half to the message then signs the + * resulting digest with RFC 6979 deterministic nonces. Returns a + * DER-encoded signature of up to 72 bytes. + * + * @param pk Public key; its type determines the signing algorithm. + * @param sk Corresponding secret key. + * @param message Raw message bytes to sign. + * @return Signature in a `Buffer` (64 bytes for Ed25519, ≤72 for secp256k1). + */ /** @{ */ Buffer sign(PublicKey const& pk, SecretKey const& sk, Slice const& message); +/** Sign a raw message, deriving the public key from `type` and `sk`. + * + * Convenience overload that calls `derivePublicKey(type, sk)` internally. + * See the primary overload for signing semantics. + * + * @param type Algorithm (`KeyType::Ed25519` or `KeyType::Secp256k1`). + * @param sk The secret key to sign with. + * @param message Raw message bytes to sign. + * @return Signature in a `Buffer` (64 bytes for Ed25519, ≤72 for secp256k1). + */ inline Buffer sign(KeyType type, SecretKey const& sk, Slice const& message) { diff --git a/include/xrpl/protocol/Seed.h b/include/xrpl/protocol/Seed.h index 0b93b84516..87ce770e2c 100644 --- a/include/xrpl/protocol/Seed.h +++ b/include/xrpl/protocol/Seed.h @@ -9,7 +9,30 @@ namespace xrpl { -/** Seeds are used to generate deterministic secret keys. */ +/** A 128-bit secret seed from which all XRPL key material is derived. + * + * A `Seed` is the root secret in the XRPL key hierarchy. From a single seed, + * a deterministic derivation produces the private key, public key, and account + * address. The class enforces two security invariants: + * + * - **No default construction.** A zero-initialized seed could be mistaken + * for valid entropy, so every `Seed` must be explicitly constructed from + * real material. + * - **Secure destruction.** The destructor calls `secure_erase()` on the + * internal buffer to overwrite key material in heap/stack memory before + * the object is released. CPU caches and registers may still retain + * remnants; this is a best-effort measure consistent with industry practice. + * + * Copy construction and assignment are allowed so seeds can be passed by + * value into key-derivation functions. Callers should minimize the number + * of live copies. + * + * Only `const` iterators and `data()` are exposed, preventing external + * mutation of the raw key material. + * + * @see randomSeed(), generateSeed(), parseGenericSeed(), parseBase58() + * @see SecretKey.h for the derivation step that consumes a Seed + */ class Seed { private: @@ -24,47 +47,60 @@ public: Seed& operator=(Seed const&) = default; - /** Destroy the seed. - The buffer will first be securely erased. - */ + /** Destroy the seed, securely erasing the internal buffer first. */ ~Seed(); - /** Construct a seed */ - /** @{ */ + /** Construct a seed from a byte slice. + * + * @param slice Raw bytes to copy into the seed buffer. + * @throws LogicError if `slice.size() != 16`. + */ explicit Seed(Slice const& slice); - explicit Seed(uint128 const& seed); - /** @} */ + /** Construct a seed from a 128-bit integer. + * + * @param seed The 128-bit value whose raw bytes are copied into the seed + * buffer. + * @throws LogicError if `seed.size() != 16`. + */ + explicit Seed(uint128 const& seed); + + /** Return a pointer to the first byte of the seed buffer. */ [[nodiscard]] std::uint8_t const* data() const { return buf_.data(); } + /** Return the size of the seed buffer in bytes (always 16). */ [[nodiscard]] std::size_t size() const { return buf_.size(); } + /** Return a const iterator to the first byte of the seed buffer. */ [[nodiscard]] const_iterator begin() const noexcept { return buf_.begin(); } + /** Return a const iterator to the first byte of the seed buffer. */ [[nodiscard]] const_iterator cbegin() const noexcept { return buf_.cbegin(); } + /** Return a const iterator past the last byte of the seed buffer. */ [[nodiscard]] const_iterator end() const noexcept { return buf_.end(); } + /** Return a const iterator past the last byte of the seed buffer. */ [[nodiscard]] const_iterator cend() const noexcept { @@ -74,42 +110,107 @@ public: //------------------------------------------------------------------------------ -/** Create a seed using secure random numbers. */ +/** Generate a cryptographically secure random seed. + * + * Fills a temporary staging buffer via `beast::rngfill()` backed by the + * global CSPRNG (`crypto_prng()`), constructs the `Seed` from it, and then + * immediately calls `secure_erase()` on the staging buffer before returning. + * The staging buffer is erased explicitly because its stack lifetime would + * otherwise extend past the point where the seed has been captured. + * + * @return A freshly generated, cryptographically random seed. + */ Seed randomSeed(); -/** Generate a seed deterministically. - - The algorithm is specific to the XRPL: - - The seed is calculated as the first 128 bits - of the SHA512-Half of the string text excluding - any terminating null. - - @note This will not attempt to determine the format of - the string (e.g. hex or base58). -*/ +/** Derive a seed deterministically from a passphrase. + * + * Implements the XRPL passphrase-to-seed algorithm: the seed is the first + * 128 bits of SHA-512-Half applied to the raw passphrase bytes (no null + * terminator included). The hasher type used (`sha512_half_hasher_s`) + * securely erases its internal state on destruction. + * + * @param passPhrase Arbitrary string treated as raw bytes; not interpreted + * as hex or Base58. + * @return The deterministic seed for the given passphrase. + * @note To parse a string that might be hex, Base58, RFC1751, or a + * passphrase, use `parseGenericSeed()` instead. + */ Seed generateSeed(std::string const& passPhrase); -/** Parse a Base58 encoded string into a seed */ +/** Decode a Base58Check-encoded seed string. + * + * Decodes a string carrying the `TokenType::FamilySeed` prefix (the + * well-known "s"-prefixed wallet seed strings). The decoded payload must + * be exactly 16 bytes; any other length yields `std::nullopt`. + * + * @param s A Base58Check-encoded string. + * @return The decoded seed, or `std::nullopt` if the string is empty, + * malformed, has an incorrect checksum, or decodes to a payload of + * the wrong size. + */ template <> std::optional parseBase58(std::string const& s); -/** Attempt to parse a string as a seed. - - @param str the string to parse - @param rfc1751 true if we should attempt RFC1751 style parsing (deprecated) - * */ +/** Parse a string in any recognized seed format. + * + * Attempts each format in order, returning on the first match: + * + * 1. **Rejection guard.** Returns `std::nullopt` if the string successfully + * parses as an `AccountID`, node public key, account public key, node + * private key, or account secret. This prevents accidentally using an + * address or public key as a seed. + * 2. **Empty string.** Returns `std::nullopt`. + * 3. **Hex.** A 32-character hex string is decoded directly as a 128-bit + * seed. + * 4. **Base58 family seed.** Delegates to `parseBase58()`. + * 5. **RFC1751 mnemonic** (only when `rfc1751 = true`). A 12-word + * English mnemonic decoded per RFC1751 with XRPL's historical + * byte-reversal convention. Parity errors cause fallthrough to the + * passphrase step rather than returning `std::nullopt`. + * 6. **Passphrase fallback.** Any non-empty string that does not match + * the above is passed to `generateSeed()`. This step always succeeds, + * so a non-empty string that is not a recognized key type will always + * produce a seed. + * + * @param str The string to parse. + * @param rfc1751 When `false`, RFC1751 mnemonic decoding is skipped. + * Pass `false` in contexts where strict format enforcement is required + * (e.g., node identity from the command line). + * @return The parsed seed, or `std::nullopt` if the string is empty or + * was recognized as a non-seed key type. + * @note The passphrase fallback means this function never returns + * `std::nullopt` for a non-empty string unless it matches a + * disallowed key type. + */ std::optional parseGenericSeed(std::string const& str, bool rfc1751 = true); -/** Encode a Seed in RFC1751 format */ +/** Encode a seed as an RFC1751 English mnemonic. + * + * Produces a 12-word phrase using the RFC1751 dictionary. XRPL reverses + * the byte order of the seed before encoding — `parseGenericSeed()` + * applies the same reversal symmetrically when decoding. + * + * @param seed The seed to encode. + * @return A space-separated 12-word RFC1751 mnemonic string. + * @note RFC1751 output is considered deprecated. `parseGenericSeed()` + * accepts it by default for backward compatibility; pass + * `rfc1751 = false` to disable that fallback. + */ std::string seedAs1751(Seed const& seed); -/** Format a seed as a Base58 string */ +/** Encode a seed as a Base58Check string with the `FamilySeed` token type. + * + * Produces the well-known "s"-prefixed wallet seed strings displayed to + * XRPL users. + * + * @param seed The seed to encode. + * @return The Base58Check-encoded seed string. + */ inline std::string toBase58(Seed const& seed) { diff --git a/include/xrpl/protocol/SeqProxy.h b/include/xrpl/protocol/SeqProxy.h index be040cceec..dd5762c278 100644 --- a/include/xrpl/protocol/SeqProxy.h +++ b/include/xrpl/protocol/SeqProxy.h @@ -1,3 +1,8 @@ +/** + * @file SeqProxy.h + * @brief Unified sequence/ticket identifier for XRPL transactions. + */ + #pragma once #include @@ -5,43 +10,65 @@ namespace xrpl { -/** A type that represents either a sequence value or a ticket value. - - We use the value() of a SeqProxy in places where a sequence was used - before. An example of this is the sequence of an Offer stored in the - ledger. We do the same thing with the in-ledger identifier of a - Check, Payment Channel, and Escrow. - - Why is this safe? If we use the SeqProxy::value(), how do we know that - each ledger entry will be unique? - - There are two components that make this safe: - - 1. A "TicketCreate" transaction carefully avoids creating a ticket - that corresponds with an already used Sequence or Ticket value. - The transactor does this by referring to the account root's - sequence number. Creating the ticket advances the account root's - sequence number so the same ticket (or sequence) value cannot be - used again. - - 2. When a "TicketCreate" transaction creates a batch of tickets it advances - the account root sequence to one past the largest created ticket. - - Therefore all tickets in a batch other than the first may never have - the same value as a sequence on that same account. And since a ticket - may only be used once there will never be any duplicates within this - account. -*/ +/** A type-tagged @c uint32_t that identifies a transaction by either a + * traditional account sequence number or a ticket sequence number. + * + * Before the Tickets feature, every XRPL transaction consumed exactly one + * account sequence number in order, so a plain @c uint32_t was sufficient. + * Tickets allow an account to pre-reserve sequence slots and use them + * out-of-order, which introduces a second namespace of transaction + * identifiers. @c SeqProxy encapsulates the choice in one place so callers + * never need to carry a separate @c bool isTicket flag. + * + * The raw @c value() is used as a ledger-object key for Offers, Checks, + * Payment Channels, and Escrows — the same role a bare sequence number + * played before tickets existed. This is safe because of two invariants + * maintained by the @c TicketCreate transactor: + * + * 1. Every ticket created has a numeric value that falls within the range + * the account root's sequence has already advanced past — so a ticket + * value can never equal any sequence number that will be consumed in the + * future by that account. + * 2. When a batch of tickets is created, the account root's sequence is + * advanced to one past the highest ticket number in the batch, permanently + * retiring all of those values from the sequence namespace. + * + * Together these guarantee that ticket values and sequence values for a + * given account never collide, even when stored without type metadata. + * + * @note The sort order imposed by @c operator< places all sequence-typed + * proxies strictly before all ticket-typed proxies, regardless of + * numeric value. @c CanonicalTXSet relies on this to ensure that + * @c TicketCreate transactions (which carry a sequence number) always + * precede the ticket-consuming transactions they enable during consensus + * replay. + * + * @see STTx::getSeqProxy() — primary production construction site + * @see CanonicalTXSet — uses SeqProxy as the per-account sort key + * @see Indexes::ticketIndex() — uses SeqProxy to derive the ledger-object key + */ class SeqProxy { public: - enum class Type : std::uint8_t { Seq = 0, Ticket }; + /** Discriminator indicating whether the proxy holds a sequence or ticket. */ + enum class Type : std::uint8_t { + Seq = 0, ///< Traditional account sequence number. + Ticket ///< Ticket sequence number (out-of-order slot). + }; private: std::uint32_t value_; Type type_; public: + /** Construct a SeqProxy with an explicit type and value. + * + * Prefer the @c sequence() factory for the common case. Ticket proxies + * are typically constructed directly: @c SeqProxy{SeqProxy::Type::Ticket, v}. + * + * @param t Whether this proxy represents a sequence or a ticket. + * @param v The numeric value of the sequence or ticket. + */ constexpr explicit SeqProxy(Type t, std::uint32_t v) : value_{v}, type_{t} { } @@ -51,35 +78,60 @@ public: SeqProxy& operator=(SeqProxy const& other) = default; - /** Factory function to return a sequence-based SeqProxy */ + /** Create a sequence-typed SeqProxy. + * + * Named factory for the common case. Ticket construction uses the + * explicit constructor directly, making it visibly intentional at each + * call site. + * + * @param v The account sequence number. + * @return A SeqProxy of type @c Type::Seq with value @c v. + */ static constexpr SeqProxy sequence(std::uint32_t v) { return SeqProxy{Type::Seq, v}; } + /** Return the raw numeric value of this proxy. + * + * Used as a ledger-object key for Offers, Checks, Payment Channels, and + * Escrows. Safe to use without the type tag because the TicketCreate + * invariants guarantee no numeric collision between sequence and ticket + * values for the same account (see class-level documentation). + * + * @return The @c uint32_t sequence or ticket number. + */ [[nodiscard]] constexpr std::uint32_t value() const { return value_; } + /** Return @c true if this proxy holds a traditional sequence number. */ [[nodiscard]] constexpr bool isSeq() const { return type_ == Type::Seq; } + /** Return @c true if this proxy holds a ticket sequence number. */ [[nodiscard]] constexpr bool isTicket() const { return type_ == Type::Ticket; } - // Occasionally it is convenient to be able to increase the value_ - // of a SeqProxy. But it's unusual. So, rather than putting in an - // addition operator, you must invoke the method by name. That makes - // if more difficult to invoke accidentally. + /** Increment the proxy's value in place and return @c *this. + * + * A named method rather than @c operator+= is deliberate: incrementing + * a @c SeqProxy is an unusual operation (currently used only in tests to + * step through a sequence of dummy transactions) and the explicit name + * prevents accidental arithmetic on what is normally a fixed identifier. + * + * @param amount Number of positions to advance the value. + * @return Reference to @c *this after the increment. + */ SeqProxy& advanceBy(std::uint32_t amount) { @@ -87,16 +139,11 @@ public: return *this; } - // Comparison - // - // The comparison is designed specifically so _all_ Sequence - // representations sort in front of Ticket representations. This - // is true even if the Ticket value() is less that the Sequence - // value(). - // - // This somewhat surprising sort order has benefits for transaction - // processing. It guarantees that transactions creating Tickets are - // sorted in from of transactions that consume Tickets. + /** Test equality — two proxies are equal only if both type and value match. + * + * A sequence proxy and a ticket proxy with the same numeric value are + * @b not equal. + */ friend constexpr bool operator==(SeqProxy lhs, SeqProxy rhs) { @@ -105,12 +152,24 @@ public: return (lhs.value() == rhs.value()); } + /** Test inequality. */ friend constexpr bool operator!=(SeqProxy lhs, SeqProxy rhs) { return !(lhs == rhs); } + /** Less-than comparison with type-first ordering. + * + * All sequence-typed proxies sort strictly before all ticket-typed + * proxies, regardless of numeric value. Within the same type, proxies + * are ordered numerically. This means even the largest possible sequence + * number (@c UINT32_MAX) sorts before the smallest ticket (@c 0). + * + * @note @c CanonicalTXSet depends on this invariant: it ensures that + * @c TicketCreate transactions (sequence-based) always precede the + * ticket-consuming transactions they enable in consensus ordering. + */ friend constexpr bool operator<(SeqProxy lhs, SeqProxy rhs) { @@ -119,24 +178,28 @@ public: return lhs.value() < rhs.value(); } + /** Greater-than comparison. */ friend constexpr bool operator>(SeqProxy lhs, SeqProxy rhs) { return rhs < lhs; } + /** Greater-than-or-equal comparison. */ friend constexpr bool operator>=(SeqProxy lhs, SeqProxy rhs) { return !(lhs < rhs); } + /** Less-than-or-equal comparison. */ friend constexpr bool operator<=(SeqProxy lhs, SeqProxy rhs) { return !(lhs > rhs); } + /** Stream a human-readable representation: @c "sequence N" or @c "ticket N". */ friend std::ostream& operator<<(std::ostream& os, SeqProxy seqProx) { diff --git a/include/xrpl/protocol/Serializer.h b/include/xrpl/protocol/Serializer.h index 81706e152a..8d2a1ef2b2 100644 --- a/include/xrpl/protocol/Serializer.h +++ b/include/xrpl/protocol/Serializer.h @@ -1,3 +1,12 @@ +/** @file + * Defines `Serializer` (write side) and `SerialIter` (read side) — the two + * classes that implement the XRPL canonical binary serialization format. + * + * Every transaction, ledger object, and signed message exchanged across the + * XRP Ledger network is encoded using this format. `Serializer` accumulates + * typed values in big-endian byte order; `SerialIter` consumes the resulting + * byte stream as a forward-only cursor. + */ #pragma once #include @@ -17,6 +26,17 @@ namespace xrpl { +/** Accumulates bytes for XRPL canonical binary serialization (write side). + * + * Every `add*` method appends data in big-endian byte order and returns the + * byte offset at which writing began, allowing callers to locate previously + * written slots for later inspection or patching. The default constructor + * pre-reserves 256 bytes to avoid reallocation on typical transaction sizes. + * + * @note The internal `Blob` (`std::vector`) storage is + * deprecated. New code should prefer zero-copy patterns built on + * `Slice` and `Buffer` where possible. + */ class Serializer { private: @@ -24,11 +44,21 @@ private: Blob data_; public: + /** Construct a serializer, pre-reserving capacity. + * + * @param n Initial byte capacity to reserve (default 256). + */ explicit Serializer(int n = 256) { data_.reserve(n); } + /** Construct a serializer pre-populated with a copy of an existing buffer. + * + * @param data Pointer to the source bytes. Must be non-null when + * `size != 0`. + * @param size Number of bytes to copy. + */ Serializer(void const* data, std::size_t size) { data_.resize(size); @@ -40,18 +70,21 @@ public: } } + /** Return a non-owning view of the accumulated bytes. */ [[nodiscard]] Slice slice() const noexcept { return Slice(data_.data(), data_.size()); } + /** Return the number of bytes accumulated so far. */ [[nodiscard]] std::size_t size() const noexcept { return data_.size(); } + /** Return a const pointer to the first accumulated byte. */ [[nodiscard]] void const* data() const noexcept { @@ -59,11 +92,33 @@ public: } // assemble functions + + /** Append a single byte in big-endian order. + * + * @param i Value to append. + * @return Byte offset at which the value was written. + */ int add8(unsigned char i); + + /** Append a 16-bit unsigned integer in big-endian byte order. + * + * @param i Value to append. + * @return Byte offset at which the value was written. + */ int add16(std::uint16_t i); + /** Append a 32-bit integer in big-endian byte order. + * + * Accepts any type whose unsigned form is exactly `uint32_t` (i.e. + * `int32_t` or `uint32_t`), preventing accidental narrowing from wider + * types at compile time. + * + * @tparam T An integer type whose unsigned counterpart is `uint32_t`. + * @param i Value to append. + * @return Byte offset at which the value was written. + */ template requires(std::is_same_v>, std::uint32_t>) int @@ -77,9 +132,30 @@ public: return ret; } + /** Append a `HashPrefix` domain-separator as a big-endian 32-bit value. + * + * Hash-domain prefixes (e.g. `TXN`, `STX`, `VAL`) are prepended to + * every signable or hashable payload to prevent cross-domain collisions. + * A `static_assert` in the implementation guards that `HashPrefix`'s + * underlying type remains `uint32_t`, which is an invariant of the wire + * format. + * + * @param p The domain-separation prefix to append. + * @return Byte offset at which the prefix was written. + */ int add32(HashPrefix p); + /** Append a 64-bit integer in big-endian byte order. + * + * Accepts any type whose unsigned form is exactly `uint64_t` (i.e. + * `int64_t` or `uint64_t`), preventing accidental narrowing at compile + * time. + * + * @tparam T An integer type whose unsigned counterpart is `uint64_t`. + * @param i Value to append. + * @return Byte offset at which the value was written. + */ template requires(std::is_same_v>, std::uint64_t>) int @@ -97,9 +173,29 @@ public: return ret; } + /** Append an integer of any supported width in big-endian byte order. + * + * Dispatches to `add8`, `add16`, `add32`, or `add64` based on `Integer`. + * Explicit specializations in the `.cpp` cover `unsigned char`, + * `uint16_t`, `uint32_t`, `int32_t`, and `uint64_t`. + * + * @tparam Integer One of the supported integer types listed above. + * @param i Value to append. + * @return Byte offset at which the value was written. + */ template int addInteger(Integer); + /** Append the raw bytes of a fixed-width integer type without any prefix. + * + * Covers `uint128`, `uint160`, `uint192`, `uint256`, and any other + * `BaseUInt` specialization. + * + * @tparam Bits Bit width of the `BaseUInt` type. + * @tparam Tag Distinguishing tag type of the `BaseUInt` specialization. + * @param v Value to append. + * @return Byte offset at which the value was written. + */ template int addBitString(BaseUInt const& v) @@ -107,29 +203,118 @@ public: return addRaw(v.data(), v.size()); } + /** Append a raw byte sequence without any length prefix. + * + * @param vector Bytes to append. + * @return Byte offset at which the data was written. + */ int addRaw(Blob const& vector); + + /** Append the bytes referenced by a `Slice` without any length prefix. + * + * @param slice Non-owning view of bytes to append. + * @return Byte offset at which the data was written. + */ int addRaw(Slice slice); + + /** Append a raw memory region without any length prefix. + * + * @param ptr Pointer to the first byte to append. + * @param len Number of bytes to copy from `ptr`. + * @return Byte offset at which the data was written. + */ int addRaw(void const* ptr, int len); + + /** Append all bytes accumulated in another `Serializer` without a length prefix. + * + * @param s Source serializer whose buffer is appended in full. + * @return Byte offset at which the data was written. + */ int addRaw(Serializer const& s); + /** Append a variable-length-prefixed blob using XRPL's three-tier VL encoding. + * + * Writes a compact 1–3 byte length header followed by the raw bytes: + * 0–192 bytes use a 1-byte header; 193–12,480 use 2 bytes; 12,481–918,744 + * use 3 bytes. + * + * @param vector Data to append. + * @return Byte offset at which the length header was written. + * @throws std::overflow_error if the data exceeds 918,744 bytes. + */ int addVL(Blob const& vector); + + /** Append a variable-length-prefixed blob from a `Slice`. + * + * Writes a compact length header then the referenced bytes. An empty + * slice writes the header only (length 0). + * + * @param slice Non-owning view of the data to append. + * @return Byte offset at which the length header was written. + * @throws std::overflow_error if the slice exceeds 918,744 bytes. + */ int addVL(Slice const& slice); + + /** Append a variable-length-prefixed blob from an iterator range. + * + * Writes the length header for a payload of `len` bytes, then iterates + * `[begin, end)` calling `addRaw` on each element's `.data()`/`.size()`. + * In debug builds an assertion verifies that the total bytes iterated + * equals `len`. + * + * @tparam Iter Forward iterator whose value type exposes `.data()` and + * `.size()`. + * @param begin Start of the range. + * @param end Past-the-end of the range. + * @param len Total byte count of all elements in the range. + * @return Byte offset at which the length header was written. + * @throws std::overflow_error if `len` exceeds 918,744. + */ template int addVL(Iter begin, Iter end, int len); + + /** Append a variable-length-prefixed blob from a raw pointer. + * + * Writes a compact length header then `len` bytes from `ptr`. When + * `len == 0` only the header is written; `ptr` is not dereferenced. + * + * @param ptr Pointer to the data to append. May be null when `len == 0`. + * @param len Number of bytes to copy. + * @return Byte offset at which the length header was written. + * @throws std::overflow_error if `len` exceeds 918,744. + */ int addVL(void const* ptr, int len); // disassemble functions - bool - get8(int&, int offset) const; + /** Read a single byte at a given offset without consuming it. + * + * @param[out] i Output parameter set to the byte value on success. + * @param offset Zero-based byte offset into the internal buffer. + * @return `true` if `offset` is within bounds; `false` otherwise. + */ + bool + get8(int& i, int offset) const; + + /** Read an integer of any supported width from the given byte offset. + * + * Assembles the value from big-endian bytes without consuming them. + * + * @tparam Integer Target integer type; must fit within the buffer from + * `offset`. + * @param[out] number Set to the decoded value on success. + * @param offset Zero-based byte offset at which to start reading. + * @return `true` if `[offset, offset + sizeof(Integer))` is within + * bounds; `false` otherwise (and `number` is unmodified). + */ template bool getInteger(Integer& number, int offset) @@ -149,6 +334,19 @@ public: return true; } + /** Copy a fixed-width integer type out of the buffer at the given offset. + * + * Uses `memcpy` directly into the `BaseUInt` storage; no byte-order + * conversion is performed, so the buffer must already contain the value + * in the expected byte order. + * + * @tparam Bits Bit width of the `BaseUInt` type. + * @tparam Tag Distinguishing tag type of the `BaseUInt` specialization. + * @param[out] data Destination for the extracted value. + * @param offset Zero-based byte offset at which to start reading. + * @return `true` if `[offset, offset + Bits/8)` is within bounds; + * `false` otherwise (and `data` is unmodified). + */ template bool getBitString(BaseUInt& data, int offset) const @@ -159,132 +357,268 @@ public: return success; } + /** Append a compact TLV field tag used by `STObject` serialization. + * + * Encodes the (type, name) pair into 1, 2, or 3 bytes: + * - Both < 16: one byte `(type << 4) | name`. + * - Type < 16, name ≥ 16: two bytes — `(type << 4)` then `name`. + * - Type ≥ 16, name < 16: two bytes — `name` then `type`. + * - Both ≥ 16: three bytes — `0x00` sentinel, then `type`, then `name`. + * + * @param type Serialized-type family code (1–255). + * @param name Per-type field index (1–255). + * @return Byte offset at which the tag was written. + * @note Both `type` and `name` must be in [1, 255]; an assertion fires in + * debug builds if either is out of range. + */ int addFieldID(int type, int name); + + /** Append a field tag using the `SerializedTypeID` enum as the type code. + * + * Convenience overload that casts `type` to `int` before delegating to + * `addFieldID(int, int)`. + * + * @param type Serialized-type family. + * @param name Per-type field index (1–255). + * @return Byte offset at which the tag was written. + */ int addFieldID(SerializedTypeID type, int name) { return addFieldID(safeCast(type), name); } + /** @deprecated Use `sha512Half(s.slice())` directly instead. + * + * Compute the XRPL "SHA-512 half" hash over the accumulated buffer. + * + * @return The first 256 bits of SHA-512 applied to the accumulated bytes. + */ // DEPRECATED [[nodiscard]] uint256 getSHA512Half() const; // totality functions + + /** Return a const reference to the underlying byte vector. + * + * @note The `Blob` type is deprecated; prefer `slice()` for new code. + */ [[nodiscard]] Blob const& peekData() const { return data_; } + + /** Return a copy of the accumulated byte vector. + * + * @note Allocates; prefer `slice()` to avoid the copy. + */ [[nodiscard]] Blob getData() const { return data_; } + + /** Return a mutable reference to the underlying byte vector. + * + * Intended for legacy callers that need to splice or overwrite bytes + * in place. New code should not use this. + */ Blob& modData() { return data_; } + /** Return the number of accumulated bytes. + * + * @note Prefer `size()` for new code. + */ [[nodiscard]] int getDataLength() const { return data_.size(); } + + /** Return a const pointer to the first accumulated byte. */ [[nodiscard]] void const* getDataPtr() const { return data_.data(); } + + /** Return a mutable pointer to the first accumulated byte. */ void* getDataPtr() { return data_.data(); } + + /** Return the number of accumulated bytes. + * + * @note Alias for `getDataLength()`; prefer `size()` for new code. + */ [[nodiscard]] int getLength() const { return data_.size(); } + + /** Return the accumulated bytes as a `std::string`. */ [[nodiscard]] std::string getString() const { return std::string(static_cast(getDataPtr()), size()); } + + /** Clear all accumulated bytes, leaving the buffer empty. */ void erase() { data_.clear(); } + + /** Remove bytes from the end of the buffer. + * + * @param num Number of bytes to remove. + * @return `true` on success; `false` if `num` exceeds the current size, + * leaving the buffer unchanged. + */ bool chop(int num); // vector-like functions - Blob ::iterator + + /** Return a mutable iterator to the first byte. */ + Blob::iterator begin() { return data_.begin(); } - Blob ::iterator + + /** Return a mutable past-the-end iterator. */ + Blob::iterator end() { return data_.end(); } - [[nodiscard]] Blob ::const_iterator + + /** Return a const iterator to the first byte. */ + [[nodiscard]] Blob::const_iterator begin() const { return data_.begin(); } - [[nodiscard]] Blob ::const_iterator + + /** Return a const past-the-end iterator. */ + [[nodiscard]] Blob::const_iterator end() const { return data_.end(); } + + /** Reserve capacity for at least `n` bytes without changing the size. + * + * @param n Minimum byte capacity to reserve. + */ void reserve(size_t n) { data_.reserve(n); } + + /** Resize the buffer to exactly `n` bytes. + * + * New bytes are zero-initialized; existing bytes beyond `n` are dropped. + * + * @param n Target size in bytes. + */ void resize(size_t n) { data_.resize(n); } + + /** Return the number of bytes that can be held without reallocation. */ [[nodiscard]] size_t capacity() const { return data_.capacity(); } + /** Compare the accumulated bytes against a raw `Blob` for equality. */ bool operator==(Blob const& v) const { return v == data_; } + + /** Compare the accumulated bytes against a raw `Blob` for inequality. */ bool operator!=(Blob const& v) const { return v != data_; } + + /** Compare two `Serializer` instances for byte-for-byte equality. */ bool operator==(Serializer const& v) const { return v.data_ == data_; } + + /** Compare two `Serializer` instances for byte-for-byte inequality. */ bool operator!=(Serializer const& v) const { return v.data_ != data_; } + /** Return the number of header bytes used to encode a VL prefix. + * + * Dispatches on the first header byte: ≤192 → 1 byte; 193–240 → 2 + * bytes; 241–254 → 3 bytes. + * + * @param b1 First byte of the VL header (0–254). + * @return 1, 2, or 3. + * @throws std::overflow_error if `b1` is negative or equals 255. + */ static int decodeLengthLength(int b1); + + /** Decode a one-byte VL length (0–192 range). + * + * @param b1 The sole header byte. + * @return The decoded payload length. + * @throws std::overflow_error if `b1` is negative or > 254. + */ static int decodeVLLength(int b1); + + /** Decode a two-byte VL length (193–12,480 range). + * + * Formula: `193 + (b1 - 193) * 256 + b2`. + * + * @param b1 First header byte (193–240). + * @param b2 Second header byte. + * @return The decoded payload length. + * @throws std::overflow_error if `b1` is outside [193, 240]. + */ static int decodeVLLength(int b1, int b2); + + /** Decode a three-byte VL length (12,481–918,744 range). + * + * Formula: `12481 + (b1 - 241) * 65536 + b2 * 256 + b3`. + * + * @param b1 First header byte (241–254). + * @param b2 Second header byte. + * @param b3 Third header byte. + * @return The decoded payload length. + * @throws std::overflow_error if `b1` is outside [241, 254]. + */ static int decodeVLLength(int b1, int b2, int b3); @@ -313,8 +647,22 @@ Serializer::addVL(Iter begin, Iter end, int len) //------------------------------------------------------------------------------ +/** Forward-only cursor over an external byte buffer for XRPL deserialization + * (read side). + * + * Stores a pointer into the caller-owned buffer together with `remain_` (bytes + * not yet consumed) and `used_` (bytes consumed). All `get*` methods advance + * the cursor and throw `std::runtime_error` on underflow — error codes are not + * returned; malformed input is treated as an exceptional condition. + * + * The buffer must outlive the iterator; no ownership is taken. `reset()` + * rewinds to the original position in O(1) using `used_` as the rewind delta. + * + * @note This class is deprecated as a direct dependency. New code should + * prefer zero-copy patterns built on `Slice` and `Buffer`. In + * particular, `getSlice()` is preferred over the copying `getRaw()`. + */ // DEPRECATED -// Transitional adapter to new serialization interfaces class SerialIter { private: @@ -323,28 +671,53 @@ private: std::size_t used_ = 0; public: + /** Construct a cursor over an existing byte buffer. + * + * The iterator does not take ownership; the caller must ensure that + * `data` remains valid for the iterator's lifetime. + * + * @param data Pointer to the first byte of the buffer. + * @param size Total number of bytes available. + */ SerialIter(void const* data, std::size_t size) noexcept; + /** Construct a cursor from a `Slice`. + * + * @param slice Non-owning view of the buffer to iterate. + */ SerialIter(Slice const& slice) : SerialIter(slice.data(), slice.size()) { } - // Infer the size of the data based on the size of the passed array. + /** Construct a cursor from a fixed-size byte array. + * + * The array size is inferred at compile time. + * + * @tparam N Size of the array (must be > 0). + * @param data Reference to the byte array. + */ template explicit SerialIter(std::uint8_t const (&data)[N]) : SerialIter(&data[0], N) { static_assert(N > 0, ""); } + /** Return `true` if all bytes have been consumed. */ [[nodiscard]] bool empty() const noexcept { return remain_ == 0; } + /** Rewind the cursor to the beginning of the buffer. + * + * O(1): uses `used_` as the rewind delta rather than storing a separate + * copy of the original pointer. + */ void reset() noexcept; + /** Return the number of bytes not yet consumed. */ [[nodiscard]] int getBytesLeft() const noexcept { @@ -352,76 +725,199 @@ public: } // get functions throw on error + + /** Consume and return the next byte. + * + * @return The byte at the current cursor position. + * @throws std::runtime_error if the buffer is exhausted. + */ unsigned char get8(); + /** Consume and decode the next 2 bytes as a big-endian unsigned 16-bit integer. + * + * @return Decoded value. + * @throws std::runtime_error if fewer than 2 bytes remain. + */ std::uint16_t get16(); + /** Consume and decode the next 4 bytes as a big-endian unsigned 32-bit integer. + * + * Use `geti32()` for signed values. + * + * @return Decoded value. + * @throws std::runtime_error if fewer than 4 bytes remain. + */ std::uint32_t get32(); + + /** Consume and decode the next 4 bytes as a big-endian signed 32-bit integer. + * + * Uses `boost::endian::load_big_s32` to ensure correct two's-complement + * sign extension. + * + * @return Decoded value. + * @throws std::runtime_error if fewer than 4 bytes remain. + */ std::int32_t geti32(); + /** Consume and decode the next 8 bytes as a big-endian unsigned 64-bit integer. + * + * Use `geti64()` for signed values. + * + * @return Decoded value. + * @throws std::runtime_error if fewer than 8 bytes remain. + */ std::uint64_t get64(); + + /** Consume and decode the next 8 bytes as a big-endian signed 64-bit integer. + * + * Uses `boost::endian::load_big_s64` to ensure correct two's-complement + * sign extension. + * + * @return Decoded value. + * @throws std::runtime_error if fewer than 8 bytes remain. + */ std::int64_t geti64(); + /** Consume and return the next `Bits/8` bytes as a `BaseUInt`. + * + * Constructs the result via `BaseUInt::fromVoid`, providing zero-copy + * extraction of fixed-width types such as `uint128`, `uint160`, `uint192`, + * and `uint256`. + * + * @tparam Bits Bit width of the target type (must be a multiple of 8). + * @tparam Tag Distinguishing tag type of the `BaseUInt` specialization. + * @return The decoded value. + * @throws std::runtime_error if fewer than `Bits/8` bytes remain. + */ template BaseUInt getBitString(); + /** Consume and return the next 16 bytes as a `uint128`. */ uint128 get128() { return getBitString<128>(); } + /** Consume and return the next 20 bytes as a `uint160`. */ uint160 get160() { return getBitString<160>(); } + /** Consume and return the next 24 bytes as a `uint192`. */ uint192 get192() { return getBitString<192>(); } + /** Consume and return the next 32 bytes as a `uint256`. */ uint256 get256() { return getBitString<256>(); } + /** Decode and consume the next field-ID tag, inverse of `Serializer::addFieldID`. + * + * Reads 1–3 bytes depending on the packing scheme. + * + * @param[out] type Decoded type family code (≥ 1). + * @param[out] name Decoded per-type field index (≥ 1). + * @throws std::runtime_error if the buffer is exhausted or a decoded + * uncommon code is < 16 (which would be ambiguous with the common + * single-byte encoding). + */ void getFieldID(int& type, int& name); - // Returns the size of the VL if the - // next object is a VL. Advances the iterator - // to the beginning of the VL. + /** Decode and consume the variable-length header, returning the payload size. + * + * Reads 1–3 header bytes and advances the cursor to the first byte of + * the payload. + * + * @return Decoded payload length in bytes. + * @throws std::runtime_error if the buffer is exhausted mid-header. + * @throws std::overflow_error if the first byte is outside the valid range. + */ int getVLDataLength(); + /** Return a zero-copy view of the next `bytes` bytes and advance the cursor. + * + * The returned `Slice` points directly into the underlying buffer and is + * valid only while that buffer is alive. Prefer this over `getRaw()` + * when an allocation can be avoided. + * + * @param bytes Number of bytes to expose. + * @return A `Slice` referencing the requested region. + * @throws std::runtime_error if `bytes` exceeds the remaining bytes. + */ Slice getSlice(std::size_t bytes); - // VFALCO DEPRECATED Returns a copy + /** @deprecated Prefer `getSlice()` to avoid allocation. + * + * Copy `size` bytes from the current position into a new `Blob` and + * advance the cursor. + * + * @param size Number of bytes to copy. + * @return A `Blob` containing the copied bytes. + * @throws std::runtime_error if `size` exceeds the remaining bytes. + */ Blob getRaw(int size); - // VFALCO DEPRECATED Returns a copy + /** @deprecated Prefer `getVLBuffer()` or `getVLDataLength()` + `getSlice()`. + * + * Decode the VL header and return a copy of the payload as a `Blob`. + * + * @return A `Blob` containing the VL payload. + * @throws std::runtime_error if the buffer is exhausted. + */ Blob getVL(); + /** Advance the cursor by `num` bytes without reading the data. + * + * @param num Number of bytes to skip. + * @throws std::runtime_error if `num` exceeds the remaining bytes. + */ void skip(int num); + /** Decode the VL header and return the payload as a move-only `Buffer`. + * + * Equivalent to `getVL()` but avoids the SSO overhead of `std::vector`. + * Prefer this over `getVL()` for new callers. + * + * @return A `Buffer` containing the VL payload. + * @throws std::runtime_error if the buffer is exhausted. + */ Buffer getVLBuffer(); + /** Copy `size` bytes from the current position into a new container of + * type `T` and advance the cursor. + * + * `T` must be either `Blob` or `Buffer`. The `size == 0` guard skips + * `memcpy` because passing a null pointer — which an empty `Buffer` may + * have — to `memcpy` with a zero count is undefined behavior in C++. + * + * @tparam T Either `Blob` or `Buffer`. + * @param size Number of bytes to copy. + * @return A freshly allocated container holding the copied bytes. + * @throws std::runtime_error if `size` exceeds the remaining bytes. + */ template T getRawHelper(int size); diff --git a/include/xrpl/protocol/Sign.h b/include/xrpl/protocol/Sign.h index 0b5b5d7239..1c2741951b 100644 --- a/include/xrpl/protocol/Sign.h +++ b/include/xrpl/protocol/Sign.h @@ -1,3 +1,19 @@ +/** @file + * Signing and verification API for XRPL serialized protocol objects. + * + * Every function here follows the same pipeline: prepend the 4-byte + * `HashPrefix` domain-separation constant, serialize the object via + * `STObject::addWithoutSigningFields()` (which omits signature-carrying + * fields to break the circular dependency), then delegate to the raw + * cryptographic primitives in `SecretKey.h` and `PublicKey.h`. + * + * The `HashPrefix` guarantees that a valid signature in one protocol + * context (e.g. a single-signed transaction via `HashPrefix::TxSign`) + * cannot be replayed as a valid signature in another (e.g. a ledger + * validation via `HashPrefix::Validation`), even if both objects happen + * to share identical serialized bytes. + */ + #pragma once #include @@ -7,17 +23,32 @@ namespace xrpl { -/** Sign an STObject - - @param st Object to sign - @param prefix Prefix to insert before serialized object when hashing - @param type Signing key type used to derive public key - @param sk Signing secret key - @param sigField Field in which to store the signature on the object. - If not specified the value defaults to `sfSignature`. - - @note If a signature already exists, it is overwritten. -*/ +/** Sign an STObject and store the resulting signature in the object. + * + * Serializes `st` via `addWithoutSigningFields()` (excluding all + * signing-related fields to avoid circularity), prepends `prefix` as a + * 4-byte domain-separation constant, then computes an asymmetric signature + * over the resulting bytes using `type` and `sk`. The produced signature is + * written into `st` at `sigField`, overwriting any pre-existing value. + * + * @param st The object to sign. Modified in place: `sigField` is set + * to the computed signature. + * @param prefix Domain-separation prefix prepended to the serialized + * payload before hashing. Must match the prefix used by callers of + * `verify()` for the same signing context (e.g. `HashPrefix::TxSign` + * for single-signed transactions, `HashPrefix::Manifest` for validator + * manifests). + * @param type Key algorithm (`secp256k1` or `ed25519`) used to sign. + * Must be consistent with the algorithm of `sk`. + * @param sk Secret key used to compute the signature. The key material + * is never copied or retained beyond the duration of this call. + * @param sigField Field in `st` that receives the signature blob. Defaults + * to `sfSignature` for standard single-signed transactions; pass an + * alternative field (e.g. `sfMasterSignature`) for other signing + * contexts such as validator manifests. + * + * @note Any existing value in `sigField` is unconditionally overwritten. + */ void sign( STObject& st, @@ -26,14 +57,23 @@ sign( SecretKey const& sk, SF_VL const& sigField = sfSignature); -/** Returns `true` if STObject contains valid signature - - @param st Signed object - @param prefix Prefix inserted before serialized object when hashing - @param pk Public key for verifying signature - @param sigField Object's field containing the signature. - If not specified the value defaults to `sfSignature`. -*/ +/** Verify that an STObject carries a valid signature. + * + * Reads the signature blob from `sigField`, regenerates the identical + * serialized payload used by `sign()` (prefix prepended to + * `addWithoutSigningFields()` output), and verifies the blob against `pk`. + * + * @param st The signed object to verify. + * @param prefix Domain-separation prefix that was prepended during signing. + * Must be the same value that was passed to `sign()`. + * @param pk Public key corresponding to the secret key used to sign. + * @param sigField Field in `st` from which to read the signature blob. + * Defaults to `sfSignature`; pass an alternative field (e.g. + * `sfMasterSignature`) to verify other signing contexts. + * @return `true` if the signature in `sigField` is cryptographically valid + * for the serialized payload and `pk`; `false` if `sigField` is absent + * or the signature does not verify. + */ bool verify( STObject const& st, @@ -41,25 +81,62 @@ verify( PublicKey const& pk, SF_VL const& sigField = sfSignature); -/** Return a Serializer suitable for computing a multisigning TxnSignature. */ +/** Build the complete multi-signing payload for a single signer. + * + * Prepends `HashPrefix::TxMultiSign`, serializes `obj` without signing + * fields, then appends `signingID` as a raw 160-bit account identifier. + * The result is equivalent to calling `startMultiSigningData` followed + * immediately by `finishMultiSigningData`. + * + * The `signingID` **must** be incorporated in the payload. Without it an + * attacker could substitute one signer slot for another account that shares + * the same `RegularKey` — a realistic threat when a custodial service + * provides a single signing key across many accounts. Binding the account + * identity into the signed data makes each authorization cryptographically + * specific to that signer slot. + * + * Use this function for single-signer contexts. For batch multi-sig + * verification, prefer `startMultiSigningData` + `finishMultiSigningData` + * to avoid redundant serialization of the shared transaction body. + * + * @param obj The transaction or object being authorized. + * @param signingID The `AccountID` of the signer authorizing `obj`. + * @return A `Serializer` containing the complete signing payload, ready + * for hashing and signing. + * @see startMultiSigningData, finishMultiSigningData + */ Serializer buildMultiSigningData(STObject const& obj, AccountID const& signingID); -/** Break the multi-signing hash computation into 2 parts for optimization. - - We can optimize verifying multiple multisignatures by splitting the - data building into two parts; - o A large part that is shared by all of the computations. - o A small part that is unique to each signer in the multisignature. - - The following methods support that optimization: - 1. startMultiSigningData provides the large part which can be shared. - 2. finishMultiSigningData caps the passed in serializer with each - signer's unique data. -*/ +/** Build the shared prefix of a multi-signing payload. + * + * Prepends `HashPrefix::TxMultiSign` and serializes `obj` without signing + * fields. The returned `Serializer` is identical for every signer of the + * same transaction; pass it to `finishMultiSigningData` once per signer to + * append only the small, signer-specific `AccountID` tail. This split avoids + * re-serializing the (potentially large) transaction body for each signer + * during batch verification. + * + * @param obj The transaction or object being authorized. + * @return A `Serializer` holding the shared signing prefix. The returned + * value must be completed with `finishMultiSigningData` before use. + * @see finishMultiSigningData, buildMultiSigningData + */ Serializer startMultiSigningData(STObject const& obj); +/** Append the per-signer suffix to a multi-signing payload in place. + * + * Writes `signingID` as a raw 160-bit bit-string onto the end of `s`, + * completing the payload started by `startMultiSigningData`. After this + * call, `s.slice()` is ready to be passed to the cryptographic sign or + * verify functions. + * + * @param signingID The `AccountID` of the signer being authorized. + * @param s The in-progress `Serializer` returned by + * `startMultiSigningData`. Modified in place. + * @see startMultiSigningData, buildMultiSigningData + */ inline void finishMultiSigningData(AccountID const& signingID, Serializer& s) { diff --git a/include/xrpl/protocol/SystemParameters.h b/include/xrpl/protocol/SystemParameters.h index 029c0418b5..cfa92dc76e 100644 --- a/include/xrpl/protocol/SystemParameters.h +++ b/include/xrpl/protocol/SystemParameters.h @@ -1,3 +1,13 @@ +/** @file + * Protocol-wide constants and validation helpers for the XRP Ledger. + * + * This header is intentionally lightweight: it is included across virtually + * the entire codebase, so it avoids heavy dependencies. Everything here is + * either a fixed property of the XRP Ledger network (total supply, earliest + * known ledger, governance thresholds) or a small convenience function built + * directly on those values. + */ + #pragma once #include @@ -8,9 +18,14 @@ namespace xrpl { -// Various protocol and system specific constant globals. - -/* The name of the system. */ +/** Return the canonical name of the XRP Ledger daemon. + * + * Uses a Meyers singleton (function-local `static`) to avoid the static + * initialization order fiasco. The `inline` specifier allows inclusion in + * multiple translation units without ODR violations. + * + * @return The string `"xrpld"`. + */ static inline std::string const& systemName() { @@ -18,29 +33,14 @@ systemName() return kNAME; } -/** Configure the native currency. */ - -/** Number of drops in the genesis account. */ -constexpr XRPAmount kINITIAL_XRP{100'000'000'000 * kDROPS_PER_XRP}; -static_assert(kINITIAL_XRP.drops() == 100'000'000'000'000'000); -static_assert(Number::kMAX_REP >= kINITIAL_XRP.drops()); - -/** Returns true if the amount does not exceed the initial XRP in existence. */ -inline bool -isLegalAmount(XRPAmount const& amount) -{ - return amount <= kINITIAL_XRP; -} - -/** Returns true if the absolute value of the amount does not exceed the initial - * XRP in existence. */ -inline bool -isLegalAmountSigned(XRPAmount const& amount) -{ - return amount >= -kINITIAL_XRP && amount <= kINITIAL_XRP; -} - -/* The currency code for the native currency. */ +/** Return the ISO currency code for the native asset. + * + * Uses a Meyers singleton (function-local `static`) for the same reasons as + * `systemName()`. Callers should prefer this over scattering `"XRP"` literals + * throughout the codebase. + * + * @return The string `"XRP"`. + */ static inline std::string const& systemCurrencyCode() { @@ -48,20 +48,97 @@ systemCurrencyCode() return kCODE; } -/** The XRP ledger network's earliest allowed sequence */ +/** Total XRP supply at ledger genesis: 100 billion XRP expressed in drops. + * + * Computed as `100'000'000'000 * kDROPS_PER_XRP` (= 10^17 drops). + * Two `static_assert`s immediately below guard that the raw bit value is + * correct and that `Number::kMAX_REP` can represent it — a compile-time + * tripwire if either the XRP total or `Number`'s internal representation + * is ever changed. + */ +constexpr XRPAmount kINITIAL_XRP{100'000'000'000 * kDROPS_PER_XRP}; +static_assert(kINITIAL_XRP.drops() == 100'000'000'000'000'000); +static_assert(Number::kMAX_REP >= kINITIAL_XRP.drops()); + +/** Return whether @p amount is within the legal unsigned XRP range. + * + * An amount is legal when it does not exceed the total XRP ever in existence. + * Called by `Transactor::preflight1` to reject fee fields that would exceed + * `kINITIAL_XRP`, and by `InvariantCheck` as a post-transaction guard. + * + * @param amount The drop amount to validate. + * @return `true` if `amount <= kINITIAL_XRP`. + */ +inline bool +isLegalAmount(XRPAmount const& amount) +{ + return amount <= kINITIAL_XRP; +} + +/** Return whether @p amount is within the legal signed XRP range. + * + * Extends `isLegalAmount` to accept negative values, which arise in delta + * and fee calculations. Used by `InvariantCheck` to ensure no ledger + * operation manufactures XRP out of thin air. + * + * @param amount The signed drop amount to validate. + * @return `true` if `amount` is in `[-kINITIAL_XRP, kINITIAL_XRP]`. + */ +inline bool +isLegalAmountSigned(XRPAmount const& amount) +{ + return amount >= -kINITIAL_XRP && amount <= kINITIAL_XRP; +} + +/** Earliest ledger sequence available on the XRP Ledger mainnet. + * + * Ledgers 1–32569 were lost in an early network incident and no longer exist + * anywhere. The `Database` class uses this value as the default lower bound + * for the `earliest_seq` configuration parameter, causing any node without + * a custom setting to refuse requests for pre-genesis sequences. + */ static constexpr std::uint32_t kXRP_LEDGER_EARLIEST_SEQ{32570u}; -/** The XRP Ledger mainnet's earliest ledger with a FeeSettings object. Only - * used in asserts and tests. */ +/** Earliest mainnet ledger sequence that contains a `FeeSettings` object. + * + * Used exclusively in `XRPL_ASSERT` calls and tests in the form: + * @code + * XRPL_ASSERT( + * ledger->header().seq < kXRP_LEDGER_EARLIEST_FEES || + * ledger->read(keylet::fees()), + * "..."); + * @endcode + * This allows the `FeeSettings` invariant to be checked on modern ledgers + * while skipping it for historical replay of early mainnet ledgers where + * the object did not yet exist. + */ static constexpr std::uint32_t kXRP_LEDGER_EARLIEST_FEES{562177u}; -/** The minimum amount of support an amendment should have. */ +/** Required validator support for an amendment to achieve majority. + * + * Represents 80% as a `std::ratio` rather than a floating-point constant + * so that `AmendmentTable` can compute the threshold count with pure integer + * arithmetic (`(trustedValidations * num) / den`), avoiding rounding error + * in this consensus-critical gate. + */ constexpr std::ratio<80, 100> kAMENDMENT_MAJORITY_CALC_THRESHOLD; -/** The minimum amount of time an amendment must hold a majority */ +/** Minimum continuous duration that an amendment must hold 80% validator + * support before it activates on mainnet. + * + * Seeds `Config::AMENDMENT_MAJORITY_TIME`, which operators may override. + * Uses the `weeks` alias from `xrpl/basics/chrono.h` (predates C++20 + * `std::chrono::weeks`). + */ constexpr std::chrono::seconds const kDEFAULT_AMENDMENT_MAJORITY_TIME = weeks{2}; } // namespace xrpl -/** Default peer port (IANA registered) */ +/** IANA-registered port for XRP Ledger peer-to-peer connections. + * + * Declared outside the `xrpl` namespace so that networking code constructing + * socket addresses can reference it without namespace qualification. + * Used by `OverlayImpl` peer discovery and the `peer_connect` RPC handler + * as the fallback when no explicit port is configured. + */ inline std::uint16_t constexpr kDEFAULT_PEER_PORT{2459}; diff --git a/include/xrpl/protocol/TER.h b/include/xrpl/protocol/TER.h index c89610f354..083fbdf8b6 100644 --- a/include/xrpl/protocol/TER.h +++ b/include/xrpl/protocol/TER.h @@ -1,3 +1,15 @@ +/** @file + * Transaction Engine Result (TER) code taxonomy for the XRP Ledger. + * + * Defines the six result-code enumerations (tel/tem/tef/ter/tes/tec), + * the strongly-typed `TERSubset` wrapper that enforces which + * categories are permitted in a given context, the `NotTEC` and `TER` + * aliases, comparison operators, and lookup utilities. + * + * Every code value is part of the wire protocol: numeric values are + * stored in ledger metadata and consumed by `ripple-binary-codec`. + * @see https://xrpl.org/transaction-results.html + */ #pragma once // NOLINTBEGIN(readability-identifier-naming) @@ -12,15 +24,29 @@ namespace xrpl { -// See https://xrpl.org/transaction-results.html -// -// "Transaction Engine Result" -// or Transaction ERror. -// +/** Underlying integer type shared by all TER code enumerations. + * + * Using a named typedef allows `TERSubset` to store a plain `int` + * without naming a specific enum, and lets `TERtoInt` overloads share + * a single return type that triggers the comparison-operator SFINAE. + * + * @see https://xrpl.org/transaction-results.html + */ using TERUnderlyingType = int; //------------------------------------------------------------------------------ +/** Local-error result codes (range −399..−300). + * + * A `tel` result means this node alone rejected the transaction; the + * decision is not propagated to the network. The transaction is not + * forwarded to peers and no fee check is performed. Common causes: + * fee below the local minimum, or path counts that exceed node-local + * limits. These codes are only valid during non-consensus processing. + * + * @note Numeric values are stable and encoded in `ripple-binary-codec`. + * Never renumber or remove existing enumerators. + */ // Protocol-critical, mixed with custom TER wrapper type, hundreds of usages // NOLINTNEXTLINE(cppcoreguidelines-use-enum-class) enum TELcodes : TERUnderlyingType { @@ -28,11 +54,6 @@ enum TELcodes : TERUnderlyingType { // Exact numbers are used in ripple-binary-codec: // https://github.com/XRPLF/xrpl.js/blob/main/packages/ripple-binary-codec/src/enums/definitions.json // Use tokens. - - // -399 .. -300: L Local error (transaction fee inadequate, exceeds local - // limit) Only valid during non-consensus processing. Implications: - // - Not forwarded - // - No fee check telLOCAL_ERROR = -399, telBAD_DOMAIN, telBAD_PATH_COUNT, @@ -54,6 +75,15 @@ enum TELcodes : TERUnderlyingType { //------------------------------------------------------------------------------ +/** Malformed-transaction result codes (range −299..−200). + * + * A `tem` result means the transaction is structurally corrupt and + * cannot succeed in any possible ledger state. The transaction is + * rejected without being applied or forwarded, and no fee is charged. + * + * @note Numeric values are stable and encoded in `ripple-binary-codec`. + * Never renumber or remove existing enumerators. + */ // Protocol-critical, mixed with custom TER wrapper type, hundreds of usages // NOLINTNEXTLINE(cppcoreguidelines-use-enum-class) enum TEMcodes : TERUnderlyingType { @@ -61,15 +91,6 @@ enum TEMcodes : TERUnderlyingType { // Exact numbers are used in ripple-binary-codec: // https://github.com/XRPLF/xrpl.js/blob/main/packages/ripple-binary-codec/src/enums/definitions.json // Use tokens. - - // -299 .. -200: M Malformed (bad signature) - // Causes: - // - Transaction corrupt. - // Implications: - // - Not applied - // - Not forwarded - // - Reject - // - Cannot succeed in any imagined ledger. temMALFORMED = -299, temBAD_AMOUNT, @@ -106,8 +127,8 @@ enum TEMcodes : TERUnderlyingType { temCANNOT_PREAUTH_SELF, temINVALID_COUNT, - temUNCERTAIN, // An internal intermediate result; should never be returned. - temUNKNOWN, // An internal intermediate result; should never be returned. + temUNCERTAIN, ///< Internal sentinel — in the process of determining a result; never returned to callers. + temUNKNOWN, ///< Internal sentinel — logic not yet implemented; never returned to callers. temSEQ_AND_TICKET, temBAD_NFTOKEN_TRANSFER_FEE, @@ -132,6 +153,17 @@ enum TEMcodes : TERUnderlyingType { //------------------------------------------------------------------------------ +/** Failure result codes (range −199..−100). + * + * A `tef` result means the transaction cannot be applied because of the + * current ledger state (e.g., sequence already used, bad signature, or + * an unexpected C++ exception). The transaction is not applied, not + * forwarded, and no fee is charged. Unlike `tem`, a `tef` transaction + * could theoretically succeed in a different ledger state. + * + * @note Numeric values are stable and encoded in `ripple-binary-codec`. + * Never renumber or remove existing enumerators. + */ // Protocol-critical, mixed with custom TER wrapper type, hundreds of usages // NOLINTNEXTLINE(cppcoreguidelines-use-enum-class) enum TEFcodes : TERUnderlyingType { @@ -139,19 +171,6 @@ enum TEFcodes : TERUnderlyingType { // Exact numbers are used in ripple-binary-codec: // https://github.com/XRPLF/xrpl.js/blob/main/packages/ripple-binary-codec/src/enums/definitions.json // Use tokens. - - // -199 .. -100: F - // Failure (sequence number previously used) - // - // Causes: - // - Transaction cannot succeed because of ledger state. - // - Unexpected ledger state. - // - C++ exception. - // - // Implications: - // - Not applied - // - Not forwarded - // - Could succeed in an imagined ledger. tefFAILURE = -199, tefALREADY, tefBAD_ADD_AUTH, @@ -178,6 +197,18 @@ enum TEFcodes : TERUnderlyingType { //------------------------------------------------------------------------------ +/** Retry result codes (range −99..−1). + * + * A `ter` result means the transaction cannot succeed right now, but + * might succeed after other transactions are applied — for example, + * if the sequence number is too high or there are insufficient funds + * for the fee. The transaction is not applied and leaves a sequence + * gap that can block later transactions. It may be held in the + * transaction queue (`terQUEUED`) to retry when fee levels drop. + * + * @note Numeric values are stable and encoded in `ripple-binary-codec`. + * Never renumber or remove existing enumerators. + */ // Protocol-critical, mixed with custom TER wrapper type, hundreds of usages // NOLINTNEXTLINE(cppcoreguidelines-use-enum-class) enum TERcodes : TERUnderlyingType { @@ -185,23 +216,6 @@ enum TERcodes : TERUnderlyingType { // Exact numbers are used in ripple-binary-codec: // https://github.com/XRPLF/xrpl.js/blob/main/packages/ripple-binary-codec/src/enums/definitions.json // Use tokens. - - // -99 .. -1: R Retry - // sequence too high, no funds for txn fee, originating -account - // non-existent - // - // Cause: - // Prior application of another, possibly non-existent, transaction could - // allow this transaction to succeed. - // - // Implications: - // - Not applied - // - May be forwarded - // - Results indicating the txn was forwarded: terQUEUED - // - All others are not forwarded. - // - Might succeed later - // - Hold - // - Makes hole in sequence which jams transactions. terRETRY = -99, terFUNDS_SPENT, // DEPRECATED. terINSUF_FEE_B, // Can't pay fee, therefore don't burden network. @@ -224,57 +238,50 @@ enum TERcodes : TERUnderlyingType { //------------------------------------------------------------------------------ +/** Success result code (value 0). + * + * `tesSUCCESS` is the sole member: the transaction was applied to the + * ledger and forwarded to peers. Its numeric value (0) is stored in + * ledger metadata and must never change. + * + * @note `TERSubset::operator bool()` returns `false` for this code + * (success = falsy), mirroring the conventional C error-code idiom. + */ // Protocol-critical, mixed with custom TER wrapper type, hundreds of usages // NOLINTNEXTLINE(cppcoreguidelines-use-enum-class) enum TEScodes : TERUnderlyingType { // Note: Exact number must stay stable. This code is stored by value // in metadata for historic transactions. - - // 0: S Success (success) - // Causes: - // - Success. - // Implications: - // - Applied - // - Forwarded tesSUCCESS = 0 }; //------------------------------------------------------------------------------ +/** Fee-claim result codes (range 100..255). + * + * A `tec` result means the fee is consumed and the sequence number is + * spent, but no other effect is applied to the ledger. The transaction + * is still applied and forwarded to peers. Typical causes: a payment + * with no valid path, or a transaction that is logically invalid but + * well-formed enough to charge a fee. + * + * When `tapRETRY` is set during application, `tec` codes are demoted + * to `terRETRY` so the transaction can be retried rather than + * consuming the sequence number. + * + * @note **DO NOT CHANGE THESE NUMBERS.** They are stored by value in + * ledger metadata and parsed by external tools such as + * `ripple-binary-codec`. Append new codes; never renumber or remove. + * @note Naming convention: use `tecNO_ENTRY` when the primary ledger + * object targeted by the transaction is missing; use + * `tecOBJECT_NOT_FOUND` when an auxiliary object required to + * complete the transaction cannot be found. + */ // Protocol-critical, mixed with custom TER wrapper type, hundreds of usages // NOLINTNEXTLINE(cppcoreguidelines-use-enum-class) enum TECcodes : TERUnderlyingType { // Note: Exact numbers must stay stable. These codes are stored by // value in metadata for historic transactions. - - // 100 .. 255 C - // Claim fee only (ripple transaction with no good paths, pay to - // non-existent account, no path) - // - // Causes: - // - Success, but does not achieve optimal result. - // - Invalid transaction or no effect, but claim fee to use the sequence - // number. - // - // Implications: - // - Applied - // - Forwarded - // - // Only allowed as a return code of appliedTransaction when !tapRETRY. - // Otherwise, treated as terRETRY. - // - // DO NOT CHANGE THESE NUMBERS: They appear in ledger meta data. - // - // Note: - // tecNO_ENTRY is often used interchangeably with tecOBJECT_NOT_FOUND. - // While there does not seem to be a clear rule which to use when, the - // following guidance will help to keep errors consistent with the - // majority of (but not all) transaction types: - // - tecNO_ENTRY : cannot find the primary ledger object on which the - // transaction is being attempted - // - tecOBJECT_NOT_FOUND : cannot find the additional object(s) needed to - // complete the transaction - tecCLAIM = 100, tecPATH_PARTIAL = 101, tecUNFUNDED_ADD = 102, // Unused legacy code @@ -362,37 +369,56 @@ enum TECcodes : TERUnderlyingType { //------------------------------------------------------------------------------ -// For generic purposes, a free function that returns the value of a TE*codes. +/** Convert a TEL/TEM/TEF/TER/TES/TEC code to its underlying integer value. + * + * These overloads form the single conversion point used by `TERSubset`, + * comparison operators, and any code that needs to inspect the raw + * integer. A free-function overload set is used rather than an explicit + * conversion operator on `TERSubset` to prevent silent implicit + * conversions in constructor-initialization contexts (e.g., `Status(TER)` + * would compile silently even with `explicit` — a named function does not). + * + * A matching `friend` overload for `TERSubset` is defined inside + * the class template; the six overloads below cover the raw enum types. + * + * @param v The enum code to convert. + * @return The underlying `int` value of `v`. + */ constexpr TERUnderlyingType TERtoInt(TELcodes v) { return safeCast(v); } +/** @copydoc TERtoInt(TELcodes) */ constexpr TERUnderlyingType TERtoInt(TEMcodes v) { return safeCast(v); } +/** @copydoc TERtoInt(TELcodes) */ constexpr TERUnderlyingType TERtoInt(TEFcodes v) { return safeCast(v); } +/** @copydoc TERtoInt(TELcodes) */ constexpr TERUnderlyingType TERtoInt(TERcodes v) { return safeCast(v); } +/** @copydoc TERtoInt(TELcodes) */ constexpr TERUnderlyingType TERtoInt(TEScodes v) { return safeCast(v); } +/** @copydoc TERtoInt(TELcodes) */ constexpr TERUnderlyingType TERtoInt(TECcodes v) { @@ -400,15 +426,37 @@ TERtoInt(TECcodes v) } //------------------------------------------------------------------------------ -// Template class that is specific to selected ranges of error codes. The -// Trait tells std::enable_if which ranges are allowed. + +/** Strongly-typed wrapper around a TER integer that restricts which + * result-code categories may be implicitly assigned or constructed. + * + * The `Trait` policy class template determines which `TE*codes` enum + * types are accepted. A specialization of `Trait` that inherits from + * `std::true_type` permits `T`; one inheriting from `std::false_type` + * rejects it at compile time via `std::enable_if`. This provides + * category-level type safety without runtime overhead. + * + * Two concrete aliases are provided: + * - `NotTEC` — permits `tel`, `tem`, `tef`, `ter`, `tes`; **excludes + * `tec`**. Used as the return type of `preflight()` to prevent fee-theft + * via unsigned transactions (see `CanCvtToNotTEC`). + * - `TER` — permits all six categories including `tec` and `NotTEC` + * (widening assignment from `NotTEC` to `TER` is always valid). + * + * Default-constructs to `tesSUCCESS`. Truthy (`operator bool`) when the + * stored code is anything other than `tesSUCCESS`. + * + * @tparam Trait A class template whose specializations for each `TE*codes` + * type inherit from `std::true_type` (allowed) or `std::false_type` + * (disallowed). + */ template